weasyprint 65.1__py3-none-any.whl → 67.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weasyprint/__init__.py +17 -7
- weasyprint/__main__.py +21 -10
- weasyprint/anchors.py +4 -4
- weasyprint/css/__init__.py +732 -67
- weasyprint/css/computed_values.py +65 -170
- weasyprint/css/counters.py +1 -1
- weasyprint/css/functions.py +206 -0
- weasyprint/css/html5_ua.css +3 -7
- weasyprint/css/html5_ua_form.css +2 -2
- weasyprint/css/media_queries.py +3 -1
- weasyprint/css/properties.py +6 -2
- weasyprint/css/{utils.py → tokens.py} +306 -397
- weasyprint/css/units.py +91 -0
- weasyprint/css/validation/__init__.py +1 -1
- weasyprint/css/validation/descriptors.py +47 -19
- weasyprint/css/validation/expanders.py +7 -8
- weasyprint/css/validation/properties.py +341 -357
- weasyprint/document.py +20 -19
- weasyprint/draw/__init__.py +56 -63
- weasyprint/draw/border.py +121 -69
- weasyprint/draw/color.py +1 -1
- weasyprint/draw/text.py +60 -41
- weasyprint/formatting_structure/boxes.py +24 -5
- weasyprint/formatting_structure/build.py +33 -45
- weasyprint/images.py +76 -62
- weasyprint/layout/__init__.py +32 -26
- weasyprint/layout/absolute.py +7 -6
- weasyprint/layout/background.py +7 -7
- weasyprint/layout/block.py +195 -152
- weasyprint/layout/column.py +19 -24
- weasyprint/layout/flex.py +54 -26
- weasyprint/layout/float.py +12 -7
- weasyprint/layout/grid.py +284 -90
- weasyprint/layout/inline.py +121 -68
- weasyprint/layout/page.py +45 -12
- weasyprint/layout/percent.py +14 -10
- weasyprint/layout/preferred.py +105 -63
- weasyprint/layout/replaced.py +9 -6
- weasyprint/layout/table.py +16 -9
- weasyprint/pdf/__init__.py +58 -18
- weasyprint/pdf/anchors.py +3 -4
- weasyprint/pdf/fonts.py +126 -69
- weasyprint/pdf/metadata.py +36 -4
- weasyprint/pdf/pdfa.py +19 -3
- weasyprint/pdf/pdfua.py +7 -115
- weasyprint/pdf/pdfx.py +83 -0
- weasyprint/pdf/stream.py +57 -49
- weasyprint/pdf/tags.py +307 -0
- weasyprint/stacking.py +14 -15
- weasyprint/svg/__init__.py +59 -32
- weasyprint/svg/bounding_box.py +4 -2
- weasyprint/svg/defs.py +4 -9
- weasyprint/svg/images.py +11 -3
- weasyprint/svg/text.py +11 -2
- weasyprint/svg/utils.py +15 -8
- weasyprint/text/constants.py +1 -1
- weasyprint/text/ffi.py +4 -3
- weasyprint/text/fonts.py +13 -5
- weasyprint/text/line_break.py +146 -43
- weasyprint/urls.py +41 -13
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/METADATA +5 -6
- weasyprint-67.0.dist-info/RECORD +77 -0
- weasyprint/draw/stack.py +0 -13
- weasyprint-65.1.dist-info/RECORD +0 -74
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/WHEEL +0 -0
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/entry_points.txt +0 -0
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -142,6 +142,8 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
|
|
142
142
|
style['display'] = ('inline', 'flow')
|
|
143
143
|
|
|
144
144
|
box = make_box(element.tag, style, [], element)
|
|
145
|
+
box.first_letter_style = style_for(element, 'first-letter')
|
|
146
|
+
box.first_line_style = style_for(element, 'first-line')
|
|
145
147
|
|
|
146
148
|
if state is None:
|
|
147
149
|
# use a list to have a shared mutable object
|
|
@@ -162,9 +164,6 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
|
|
162
164
|
# names will be in this new list
|
|
163
165
|
counter_scopes.append(set())
|
|
164
166
|
|
|
165
|
-
box.first_letter_style = style_for(element, 'first-letter')
|
|
166
|
-
box.first_line_style = style_for(element, 'first-line')
|
|
167
|
-
|
|
168
167
|
marker_boxes = []
|
|
169
168
|
if 'list-item' in style['display']:
|
|
170
169
|
marker_boxes = list(marker_to_box(
|
|
@@ -195,9 +194,10 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
|
|
195
194
|
footnote = child_boxes[0]
|
|
196
195
|
footnote.style['float'] = 'none'
|
|
197
196
|
footnotes.append(footnote)
|
|
198
|
-
call_style = style_for(element, 'footnote-call')
|
|
197
|
+
call_style = style_for(footnote.element, 'footnote-call')
|
|
199
198
|
footnote_call = make_box(
|
|
200
|
-
f'{element.tag}::footnote-call', call_style, [],
|
|
199
|
+
f'{footnote.element.tag}::footnote-call', call_style, [],
|
|
200
|
+
footnote.element)
|
|
201
201
|
footnote_call.children = content_to_boxes(
|
|
202
202
|
call_style, footnote_call, quote_depth, counter_values,
|
|
203
203
|
get_image_from_uri, target_collector, counter_style)
|
|
@@ -250,7 +250,7 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
|
|
250
250
|
marker = make_box(
|
|
251
251
|
f'{element.tag}::footnote-marker', marker_style, [], element)
|
|
252
252
|
marker.children = content_to_boxes(
|
|
253
|
-
marker_style,
|
|
253
|
+
marker_style, marker, quote_depth, counter_values, get_image_from_uri,
|
|
254
254
|
target_collector, counter_style)
|
|
255
255
|
box.children.insert(0, marker)
|
|
256
256
|
|
|
@@ -344,7 +344,6 @@ def marker_to_box(element, state, parent_style, style_for, get_image_from_uri,
|
|
|
344
344
|
if not children and style['list_style_type'] != 'none':
|
|
345
345
|
counter_value = counter_values.get('list-item', [0])[-1]
|
|
346
346
|
counter_type = style['list_style_type']
|
|
347
|
-
# TODO: rtl numbered list has the dot on the left
|
|
348
347
|
if marker_text := counter_style.render_marker(counter_type, counter_value):
|
|
349
348
|
box = boxes.TextBox.anonymous_from(box, marker_text)
|
|
350
349
|
box.style['white_space'] = 'pre-wrap'
|
|
@@ -358,13 +357,7 @@ def marker_to_box(element, state, parent_style, style_for, get_image_from_uri,
|
|
|
358
357
|
# We can safely edit everything that can't be changed by user style
|
|
359
358
|
# See https://drafts.csswg.org/css-pseudo-4/#marker-pseudo
|
|
360
359
|
marker_box.style['position'] = 'absolute'
|
|
361
|
-
|
|
362
|
-
translate_x = properties.Dimension(-100, '%')
|
|
363
|
-
else:
|
|
364
|
-
translate_x = properties.Dimension(100, '%')
|
|
365
|
-
translate_y = properties.ZERO_PIXELS
|
|
366
|
-
marker_box.style['transform'] = (
|
|
367
|
-
('translate', (translate_x, translate_y)),)
|
|
360
|
+
marker_box.is_outside_marker = True
|
|
368
361
|
else:
|
|
369
362
|
marker_box = boxes.InlineBox.anonymous_from(box, children)
|
|
370
363
|
yield marker_box
|
|
@@ -421,7 +414,7 @@ def compute_content_list(content_list, parent_box, counter_values, css_token,
|
|
|
421
414
|
elif type_ == 'url' and get_image_from_uri is not None:
|
|
422
415
|
origin, uri = value
|
|
423
416
|
if origin != 'external':
|
|
424
|
-
# Embedding internal references is impossible
|
|
417
|
+
# Embedding internal references is impossible.
|
|
425
418
|
continue
|
|
426
419
|
image = get_image_from_uri(
|
|
427
420
|
url=uri, orientation=parent_box.style['image_orientation'])
|
|
@@ -431,12 +424,12 @@ def compute_content_list(content_list, parent_box, counter_values, css_token,
|
|
|
431
424
|
elif type_ == 'content()':
|
|
432
425
|
added_text = extract_text(value, parent_box)
|
|
433
426
|
# Simulate the step of white space processing
|
|
434
|
-
# (normally done during the layout)
|
|
427
|
+
# (normally done during the layout).
|
|
435
428
|
add_text(added_text.strip())
|
|
436
429
|
elif type_ == 'string()':
|
|
437
430
|
if not in_page_context:
|
|
438
|
-
# string() is currently only valid in @page context
|
|
439
|
-
# See
|
|
431
|
+
# string() is currently only valid in @page context.
|
|
432
|
+
# See issue #723.
|
|
440
433
|
LOGGER.warning(
|
|
441
434
|
'"string(%s)" is only allowed in page margins',
|
|
442
435
|
' '.join(value))
|
|
@@ -810,9 +803,9 @@ def table_boxes_children(box, children):
|
|
|
810
803
|
children = [
|
|
811
804
|
child
|
|
812
805
|
for prev_child, child, next_child in zip(
|
|
813
|
-
[None
|
|
806
|
+
[None, *children[:-1]],
|
|
814
807
|
children,
|
|
815
|
-
children[1:]
|
|
808
|
+
[*children[1:], None]
|
|
816
809
|
)
|
|
817
810
|
if not (
|
|
818
811
|
# Ignore some whitespace: rule 1.4
|
|
@@ -990,6 +983,24 @@ def wrap_table(box, children):
|
|
|
990
983
|
return wrapper
|
|
991
984
|
|
|
992
985
|
|
|
986
|
+
def blockify(box, layout):
|
|
987
|
+
"""Turn an inline box into a block box."""
|
|
988
|
+
# See https://drafts.csswg.org/css-display-4/#blockify.
|
|
989
|
+
if isinstance(box, boxes.InlineBlockBox):
|
|
990
|
+
anonymous = boxes.BlockBox.anonymous_from(box, box.children)
|
|
991
|
+
elif isinstance(box, boxes.InlineReplacedBox):
|
|
992
|
+
replacement = box.replacement
|
|
993
|
+
anonymous = boxes.BlockReplacedBox.anonymous_from(box, replacement)
|
|
994
|
+
elif isinstance(box, boxes.InlineLevelBox):
|
|
995
|
+
anonymous = boxes.BlockBox.anonymous_from(box, [box])
|
|
996
|
+
setattr(box, f'is_{layout}_item', False)
|
|
997
|
+
else:
|
|
998
|
+
return box
|
|
999
|
+
anonymous.style = box.style
|
|
1000
|
+
setattr(anonymous, f'is_{layout}_item', True)
|
|
1001
|
+
return anonymous
|
|
1002
|
+
|
|
1003
|
+
|
|
993
1004
|
def flex_boxes(box):
|
|
994
1005
|
"""Remove and add boxes according to the flex model.
|
|
995
1006
|
|
|
@@ -1019,18 +1030,7 @@ def flex_children(box, children):
|
|
|
1019
1030
|
# affected by the white-space property"
|
|
1020
1031
|
# https://www.w3.org/TR/css-flexbox-1/#flex-items
|
|
1021
1032
|
continue
|
|
1022
|
-
|
|
1023
|
-
anonymous = boxes.BlockBox.anonymous_from(child, child.children)
|
|
1024
|
-
anonymous.style = child.style
|
|
1025
|
-
anonymous.is_flex_item = True
|
|
1026
|
-
flex_children.append(anonymous)
|
|
1027
|
-
elif isinstance(child, boxes.InlineLevelBox):
|
|
1028
|
-
anonymous = boxes.BlockBox.anonymous_from(child, [child])
|
|
1029
|
-
anonymous.style = child.style
|
|
1030
|
-
anonymous.is_flex_item = True
|
|
1031
|
-
flex_children.append(anonymous)
|
|
1032
|
-
else:
|
|
1033
|
-
flex_children.append(child)
|
|
1033
|
+
flex_children.append(blockify(child, 'flex'))
|
|
1034
1034
|
return flex_children
|
|
1035
1035
|
else:
|
|
1036
1036
|
return children
|
|
@@ -1064,19 +1064,7 @@ def grid_children(box, children):
|
|
|
1064
1064
|
# affected by the white-space property"
|
|
1065
1065
|
# https://drafts.csswg.org/css-grid-2/#grid-item
|
|
1066
1066
|
continue
|
|
1067
|
-
|
|
1068
|
-
anonymous = boxes.BlockBox.anonymous_from(child, child.children)
|
|
1069
|
-
anonymous.style = child.style
|
|
1070
|
-
anonymous.is_grid_item = True
|
|
1071
|
-
grid_children.append(anonymous)
|
|
1072
|
-
elif isinstance(child, boxes.InlineLevelBox):
|
|
1073
|
-
anonymous = boxes.BlockBox.anonymous_from(child, [child])
|
|
1074
|
-
anonymous.style = child.style
|
|
1075
|
-
child.is_grid_item = False
|
|
1076
|
-
anonymous.is_grid_item = True
|
|
1077
|
-
grid_children.append(anonymous)
|
|
1078
|
-
else:
|
|
1079
|
-
grid_children.append(child)
|
|
1067
|
+
grid_children.append(blockify(child, 'grid'))
|
|
1080
1068
|
return grid_children
|
|
1081
1069
|
else:
|
|
1082
1070
|
return children
|
weasyprint/images.py
CHANGED
|
@@ -6,15 +6,12 @@ import struct
|
|
|
6
6
|
from hashlib import md5
|
|
7
7
|
from io import BytesIO
|
|
8
8
|
from itertools import cycle
|
|
9
|
-
from math import inf
|
|
10
9
|
from pathlib import Path
|
|
11
|
-
from urllib.parse import urlparse
|
|
12
|
-
from urllib.request import url2pathname
|
|
13
10
|
from xml.etree import ElementTree
|
|
14
11
|
|
|
15
12
|
import pydyf
|
|
16
13
|
from PIL import Image, ImageFile, ImageOps
|
|
17
|
-
from tinycss2.
|
|
14
|
+
from tinycss2.color5 import parse_color
|
|
18
15
|
|
|
19
16
|
from . import DEFAULT_OPTIONS
|
|
20
17
|
from .layout.percent import percentage
|
|
@@ -65,12 +62,12 @@ class RasterImage:
|
|
|
65
62
|
self.mode = pillow_image.mode
|
|
66
63
|
self.width = pillow_image.width
|
|
67
64
|
self.height = pillow_image.height
|
|
68
|
-
self.ratio = (self.width / self.height) if self.height != 0 else inf
|
|
65
|
+
self.ratio = (self.width / self.height) if self.height != 0 else math.inf
|
|
69
66
|
self.optimize = optimize = options['optimize_images']
|
|
70
67
|
|
|
71
68
|
# The presence of the APP14 segment indicates an Adobe image with
|
|
72
69
|
# inverted CMYK data. Specify a Decode Array to invert it again back to
|
|
73
|
-
# normal. See
|
|
70
|
+
# normal. See PR #2179.
|
|
74
71
|
app14 = getattr(original_pillow_image, 'app', {}).get('APP14')
|
|
75
72
|
self.invert_colors = self.mode == 'CMYK' and app14 is not None
|
|
76
73
|
|
|
@@ -96,10 +93,11 @@ class RasterImage:
|
|
|
96
93
|
def get_intrinsic_size(self, resolution, font_size):
|
|
97
94
|
return self.width / resolution, self.height / resolution, self.ratio
|
|
98
95
|
|
|
99
|
-
def draw(self, stream, concrete_width, concrete_height,
|
|
96
|
+
def draw(self, stream, concrete_width, concrete_height, style):
|
|
100
97
|
if self.width <= 0 or self.height <= 0:
|
|
101
98
|
return
|
|
102
99
|
|
|
100
|
+
image_rendering = style['image_rendering']
|
|
103
101
|
interpolate = image_rendering == 'auto'
|
|
104
102
|
ratio = 1
|
|
105
103
|
if self._dpi:
|
|
@@ -258,7 +256,8 @@ class LazyLocalImage(pydyf.Object):
|
|
|
258
256
|
|
|
259
257
|
class SVGImage:
|
|
260
258
|
def __init__(self, tree, base_url, url_fetcher, context):
|
|
261
|
-
|
|
259
|
+
font_config = context.font_config if context else None
|
|
260
|
+
self._svg = SVG(tree, base_url, font_config)
|
|
262
261
|
self._base_url = base_url
|
|
263
262
|
self._url_fetcher = url_fetcher
|
|
264
263
|
self._context = context
|
|
@@ -281,7 +280,7 @@ class SVGImage:
|
|
|
281
280
|
ratio = 1
|
|
282
281
|
return width, height, ratio
|
|
283
282
|
|
|
284
|
-
def draw(self, stream, concrete_width, concrete_height,
|
|
283
|
+
def draw(self, stream, concrete_width, concrete_height, _style):
|
|
285
284
|
try:
|
|
286
285
|
self._svg.draw(
|
|
287
286
|
stream, concrete_width, concrete_height, self._base_url,
|
|
@@ -299,11 +298,6 @@ def get_image_from_uri(cache, url_fetcher, options, url, forced_mime_type=None,
|
|
|
299
298
|
|
|
300
299
|
try:
|
|
301
300
|
with fetch(url_fetcher, url) as result:
|
|
302
|
-
parsed_url = urlparse(result.get('redirected_url'))
|
|
303
|
-
if parsed_url.scheme == 'file':
|
|
304
|
-
filename = url2pathname(parsed_url.path)
|
|
305
|
-
else:
|
|
306
|
-
filename = None
|
|
307
301
|
if 'string' in result:
|
|
308
302
|
string = result['string']
|
|
309
303
|
else:
|
|
@@ -337,9 +331,9 @@ def get_image_from_uri(cache, url_fetcher, options, url, forced_mime_type=None,
|
|
|
337
331
|
else:
|
|
338
332
|
# Store image id to enable cache in Stream.add_image
|
|
339
333
|
image_id = md5(url.encode(), usedforsecurity=False).hexdigest()
|
|
334
|
+
path = result.get('path')
|
|
340
335
|
image = RasterImage(
|
|
341
|
-
pillow_image, image_id, string,
|
|
342
|
-
orientation, options)
|
|
336
|
+
pillow_image, image_id, string, path, cache, orientation, options)
|
|
343
337
|
|
|
344
338
|
except (URLFetchingError, ImageLoadingError) as exception:
|
|
345
339
|
LOGGER.error('Failed to load image at %r: %s', url, exception)
|
|
@@ -374,8 +368,8 @@ def rotate_pillow_image(pillow_image, orientation):
|
|
|
374
368
|
return pillow_image
|
|
375
369
|
|
|
376
370
|
|
|
377
|
-
def process_color_stops(vector_length, positions):
|
|
378
|
-
"""Give color stops positions on the gradient vector.
|
|
371
|
+
def process_color_stops(vector_length, positions, hints, style):
|
|
372
|
+
"""Give color stops positions and hints on the gradient vector.
|
|
379
373
|
|
|
380
374
|
``vector_length`` is the distance between the starting point and ending
|
|
381
375
|
point of the vector gradient.
|
|
@@ -388,16 +382,17 @@ def process_color_stops(vector_length, positions):
|
|
|
388
382
|
Return processed color stops, as a list of floats in px.
|
|
389
383
|
|
|
390
384
|
"""
|
|
391
|
-
# Resolve percentages
|
|
392
|
-
positions = [percentage(position, vector_length) for position in positions]
|
|
385
|
+
# Resolve percentages.
|
|
386
|
+
positions = [percentage(position, style, vector_length) for position in positions]
|
|
387
|
+
hints = [percentage(hint, style, vector_length) / vector_length for hint in hints]
|
|
393
388
|
|
|
394
|
-
# First and last default to 100
|
|
389
|
+
# First and last default to 100%.
|
|
395
390
|
if positions[0] is None:
|
|
396
391
|
positions[0] = 0
|
|
397
392
|
if positions[-1] is None:
|
|
398
393
|
positions[-1] = vector_length
|
|
399
394
|
|
|
400
|
-
# Make sure positions are increasing
|
|
395
|
+
# Make sure positions are increasing.
|
|
401
396
|
previous_pos = positions[0]
|
|
402
397
|
for i, position in enumerate(positions):
|
|
403
398
|
if position is not None:
|
|
@@ -406,7 +401,7 @@ def process_color_stops(vector_length, positions):
|
|
|
406
401
|
else:
|
|
407
402
|
previous_pos = position
|
|
408
403
|
|
|
409
|
-
# Assign missing values
|
|
404
|
+
# Assign missing values.
|
|
410
405
|
previous_i = -1
|
|
411
406
|
for i, position in enumerate(positions):
|
|
412
407
|
if position is not None:
|
|
@@ -416,7 +411,13 @@ def process_color_stops(vector_length, positions):
|
|
|
416
411
|
positions[j] = base + j * increment
|
|
417
412
|
previous_i = i
|
|
418
413
|
|
|
419
|
-
|
|
414
|
+
# Calculate exponential value for PDF hints, avoid big numbers.
|
|
415
|
+
hints = [
|
|
416
|
+
0 if hint <= 0 else
|
|
417
|
+
2 ** 32 if hint >= 1 else
|
|
418
|
+
min(2 ** 32, math.log(0.5, hint)) for hint in hints]
|
|
419
|
+
|
|
420
|
+
return positions, hints
|
|
420
421
|
|
|
421
422
|
|
|
422
423
|
def normalize_stop_positions(positions):
|
|
@@ -473,21 +474,23 @@ def gradient_average_color(colors, positions):
|
|
|
473
474
|
|
|
474
475
|
|
|
475
476
|
class Gradient:
|
|
476
|
-
def __init__(self, color_stops, repeating):
|
|
477
|
+
def __init__(self, color_stops, repeating, color_hints):
|
|
477
478
|
assert color_stops
|
|
478
479
|
# List of (r, g, b, a)
|
|
479
480
|
self.colors = tuple(color for color, _ in color_stops)
|
|
480
481
|
# List of Dimensions
|
|
481
482
|
self.stop_positions = tuple(position for _, position in color_stops)
|
|
483
|
+
# List of Dimensions
|
|
484
|
+
self.color_hints = color_hints
|
|
482
485
|
# Boolean
|
|
483
486
|
self.repeating = repeating
|
|
484
487
|
|
|
485
488
|
def get_intrinsic_size(self, image_resolution, font_size):
|
|
486
489
|
return None, None, None
|
|
487
490
|
|
|
488
|
-
def draw(self, stream, concrete_width, concrete_height,
|
|
489
|
-
scale_y, type_, points, positions, colors = self.layout(
|
|
490
|
-
concrete_width, concrete_height)
|
|
491
|
+
def draw(self, stream, concrete_width, concrete_height, style):
|
|
492
|
+
scale_y, type_, points, positions, colors, color_hints = self.layout(
|
|
493
|
+
concrete_width, concrete_height, style)
|
|
491
494
|
|
|
492
495
|
if type_ == 'solid':
|
|
493
496
|
stream.rectangle(0, 0, concrete_width, concrete_height)
|
|
@@ -497,11 +500,11 @@ class Gradient:
|
|
|
497
500
|
|
|
498
501
|
alphas = [color[3] for color in colors]
|
|
499
502
|
alpha_couples = [
|
|
500
|
-
|
|
503
|
+
[alphas[i], alphas[i + 1], color_hints[i]]
|
|
501
504
|
for i in range(len(alphas) - 1)]
|
|
502
505
|
# TODO: handle other color spaces.
|
|
503
506
|
color_couples = [
|
|
504
|
-
[colors[i].to('srgb')[:3], colors[i + 1].to('srgb')[:3],
|
|
507
|
+
[colors[i].to('srgb')[:3], colors[i + 1].to('srgb')[:3], color_hints[i]]
|
|
505
508
|
for i in range(len(colors) - 1)]
|
|
506
509
|
|
|
507
510
|
# Premultiply colors
|
|
@@ -511,7 +514,7 @@ class Gradient:
|
|
|
511
514
|
color_couples[i - 1][1] = color_couples[i - 1][0]
|
|
512
515
|
if i < len(colors) - 1:
|
|
513
516
|
color_couples[i][0] = color_couples[i][1]
|
|
514
|
-
for i, (a0, a1) in enumerate(alpha_couples):
|
|
517
|
+
for i, (a0, a1, hint) in enumerate(alpha_couples):
|
|
515
518
|
if 0 not in (a0, a1) and (a0, a1) != (1, 1):
|
|
516
519
|
color_couples[i][2] = a0 / a1
|
|
517
520
|
|
|
@@ -521,8 +524,8 @@ class Gradient:
|
|
|
521
524
|
encode = (len(colors) - 1) * (0, 1)
|
|
522
525
|
bounds = positions[1:-1]
|
|
523
526
|
sub_functions = (
|
|
524
|
-
stream.create_interpolation_function((0, 1), c0, c1,
|
|
525
|
-
for c0, c1,
|
|
527
|
+
stream.create_interpolation_function((0, 1), c0, c1, hint)
|
|
528
|
+
for c0, c1, hint in color_couples)
|
|
526
529
|
function = stream.create_stitching_function(
|
|
527
530
|
domain, encode, bounds, sub_functions)
|
|
528
531
|
# TODO: handle other color spaces.
|
|
@@ -536,8 +539,8 @@ class Gradient:
|
|
|
536
539
|
|
|
537
540
|
shading_type = 2 if type_ == 'linear' else 3
|
|
538
541
|
sub_functions = (
|
|
539
|
-
stream.create_interpolation_function((0, 1), (c0,), (c1,),
|
|
540
|
-
for c0, c1 in alpha_couples)
|
|
542
|
+
stream.create_interpolation_function((0, 1), (c0,), (c1,), hint)
|
|
543
|
+
for c0, c1, hint in alpha_couples)
|
|
541
544
|
function = stream.create_stitching_function(
|
|
542
545
|
domain, encode, bounds, sub_functions)
|
|
543
546
|
alpha_shading = alpha_stream.add_shading(
|
|
@@ -547,10 +550,11 @@ class Gradient:
|
|
|
547
550
|
|
|
548
551
|
stream.paint_shading(shading.id)
|
|
549
552
|
|
|
550
|
-
def layout(self, width, height):
|
|
553
|
+
def layout(self, width, height, style):
|
|
551
554
|
"""Get layout information about the gradient.
|
|
552
555
|
|
|
553
556
|
width, height: Gradient box. Top-left is at coordinates (0, 0).
|
|
557
|
+
style: box computed style.
|
|
554
558
|
|
|
555
559
|
Returns (scale_y, type_, points, positions, colors).
|
|
556
560
|
|
|
@@ -572,15 +576,15 @@ class Gradient:
|
|
|
572
576
|
|
|
573
577
|
|
|
574
578
|
class LinearGradient(Gradient):
|
|
575
|
-
def __init__(self, color_stops, direction, repeating):
|
|
576
|
-
|
|
579
|
+
def __init__(self, color_stops, direction, repeating, color_hints):
|
|
580
|
+
super().__init__(color_stops, repeating, color_hints)
|
|
577
581
|
# ('corner', keyword) or ('angle', radians)
|
|
578
582
|
self.direction_type, self.direction = direction
|
|
579
583
|
|
|
580
|
-
def layout(self, width, height):
|
|
584
|
+
def layout(self, width, height, style):
|
|
581
585
|
# Only one color, render the gradient as a solid color
|
|
582
586
|
if len(self.colors) == 1:
|
|
583
|
-
return 1, 'solid', None, [], [self.colors[0]]
|
|
587
|
+
return 1, 'solid', None, [], [self.colors[0]], []
|
|
584
588
|
|
|
585
589
|
# Define the (dx, dy) unit vector giving the direction of the gradient.
|
|
586
590
|
# Positive dx: right, positive dy: down.
|
|
@@ -606,16 +610,19 @@ class LinearGradient(Gradient):
|
|
|
606
610
|
# Normalize colors positions
|
|
607
611
|
colors = list(self.colors)
|
|
608
612
|
vector_length = abs(width * dx) + abs(height * dy)
|
|
609
|
-
positions = process_color_stops(
|
|
613
|
+
positions, hints = process_color_stops(
|
|
614
|
+
vector_length, self.stop_positions, self.color_hints, style)
|
|
610
615
|
if not self.repeating:
|
|
611
616
|
# Add explicit colors at boundaries if needed, because PDF doesn’t
|
|
612
617
|
# extend color stops that are not displayed
|
|
613
618
|
if positions[0] == positions[1]:
|
|
614
619
|
positions.insert(0, positions[0] - 1)
|
|
615
620
|
colors.insert(0, colors[0])
|
|
621
|
+
hints.insert(0, 1)
|
|
616
622
|
if positions[-2] == positions[-1]:
|
|
617
623
|
positions.append(positions[-1] + 1)
|
|
618
624
|
colors.append(colors[-1])
|
|
625
|
+
hints.append(1)
|
|
619
626
|
first, last, positions = normalize_stop_positions(positions)
|
|
620
627
|
|
|
621
628
|
if self.repeating:
|
|
@@ -623,7 +630,7 @@ class LinearGradient(Gradient):
|
|
|
623
630
|
# See https://drafts.csswg.org/css-images-3/#repeating-gradients
|
|
624
631
|
if first == last:
|
|
625
632
|
color = gradient_average_color(colors, positions)
|
|
626
|
-
return 1, 'solid', None, [], [color]
|
|
633
|
+
return 1, 'solid', None, [], [color], []
|
|
627
634
|
|
|
628
635
|
# Define defined gradient length and steps between positions
|
|
629
636
|
stop_length = last - first
|
|
@@ -635,13 +642,16 @@ class LinearGradient(Gradient):
|
|
|
635
642
|
# Create cycles used to add colors
|
|
636
643
|
next_steps = cycle((0, *position_steps))
|
|
637
644
|
next_colors = cycle(colors)
|
|
645
|
+
next_hints = cycle(hints)
|
|
638
646
|
previous_steps = cycle((0, *position_steps[::-1]))
|
|
639
647
|
previous_colors = cycle(colors[::-1])
|
|
648
|
+
previous_hints = cycle(hints[::-1])
|
|
640
649
|
|
|
641
650
|
# Add colors after last step
|
|
642
651
|
while last < vector_length:
|
|
643
652
|
step = next(next_steps)
|
|
644
653
|
colors.append(next(next_colors))
|
|
654
|
+
hints.append(next(next_hints))
|
|
645
655
|
positions.append(positions[-1] + step)
|
|
646
656
|
last += step * stop_length
|
|
647
657
|
|
|
@@ -649,6 +659,7 @@ class LinearGradient(Gradient):
|
|
|
649
659
|
while first > 0:
|
|
650
660
|
step = next(previous_steps)
|
|
651
661
|
colors.insert(0, next(previous_colors))
|
|
662
|
+
hints.insert(0, next(previous_hints))
|
|
652
663
|
positions.insert(0, positions[0] - step)
|
|
653
664
|
first -= step * stop_length
|
|
654
665
|
|
|
@@ -659,12 +670,12 @@ class LinearGradient(Gradient):
|
|
|
659
670
|
start_x + dx * first, start_y + dy * first,
|
|
660
671
|
start_x + dx * last, start_y + dy * last)
|
|
661
672
|
|
|
662
|
-
return 1, 'linear', points, positions, colors
|
|
673
|
+
return 1, 'linear', points, positions, colors, hints
|
|
663
674
|
|
|
664
675
|
|
|
665
676
|
class RadialGradient(Gradient):
|
|
666
|
-
def __init__(self, color_stops, shape, size, center, repeating):
|
|
667
|
-
|
|
677
|
+
def __init__(self, color_stops, shape, size, center, repeating, color_hints):
|
|
678
|
+
super().__init__(color_stops, repeating, color_hints)
|
|
668
679
|
# Center of the ending shape. (origin_x, pos_x, origin_y, pos_y)
|
|
669
680
|
self.center = center
|
|
670
681
|
# Type of ending shape: 'circle' or 'ellipse'
|
|
@@ -676,15 +687,15 @@ class RadialGradient(Gradient):
|
|
|
676
687
|
# size: (radius_x, radius_y)
|
|
677
688
|
self.size_type, self.size = size
|
|
678
689
|
|
|
679
|
-
def layout(self, width, height):
|
|
690
|
+
def layout(self, width, height, style):
|
|
680
691
|
# Only one color, render the gradient as a solid color
|
|
681
692
|
if len(self.colors) == 1:
|
|
682
|
-
return 1, 'solid', None, [], [self.colors[0]]
|
|
693
|
+
return 1, 'solid', None, [], [self.colors[0]], []
|
|
683
694
|
|
|
684
695
|
# Define the center of the gradient
|
|
685
696
|
origin_x, center_x, origin_y, center_y = self.center
|
|
686
|
-
center_x = percentage(center_x, width)
|
|
687
|
-
center_y = percentage(center_y, height)
|
|
697
|
+
center_x = percentage(center_x, style, width)
|
|
698
|
+
center_y = percentage(center_y, style, height)
|
|
688
699
|
if origin_x == 'right':
|
|
689
700
|
center_x = width - center_x
|
|
690
701
|
if origin_y == 'bottom':
|
|
@@ -692,21 +703,24 @@ class RadialGradient(Gradient):
|
|
|
692
703
|
|
|
693
704
|
# Resolve sizes and vertical scale
|
|
694
705
|
size_x, size_y = self._handle_degenerate(
|
|
695
|
-
*self._resolve_size(width, height, center_x, center_y))
|
|
706
|
+
*self._resolve_size(width, height, center_x, center_y, style))
|
|
696
707
|
scale_y = size_y / size_x
|
|
697
708
|
|
|
698
709
|
# Normalize colors positions
|
|
699
710
|
colors = list(self.colors)
|
|
700
|
-
positions = process_color_stops(
|
|
711
|
+
positions, hints = process_color_stops(
|
|
712
|
+
size_x, self.stop_positions, self.color_hints, style)
|
|
701
713
|
if not self.repeating:
|
|
702
714
|
# Add explicit colors at boundaries if needed, because PDF doesn’t
|
|
703
715
|
# extend color stops that are not displayed
|
|
704
716
|
if positions[0] > 0 and positions[0] == positions[1]:
|
|
705
717
|
positions.insert(0, 0)
|
|
706
718
|
colors.insert(0, colors[0])
|
|
719
|
+
hints.insert(0, 1)
|
|
707
720
|
if positions[-2] == positions[-1]:
|
|
708
721
|
positions.append(positions[-1] + 1)
|
|
709
722
|
colors.append(colors[-1])
|
|
723
|
+
hints.append(1)
|
|
710
724
|
if positions[0] < 0:
|
|
711
725
|
# PDF doesn’t like negative radiuses, shift into the positive realm
|
|
712
726
|
if self.repeating:
|
|
@@ -718,7 +732,7 @@ class RadialGradient(Gradient):
|
|
|
718
732
|
# Only keep colors with position >= 0, interpolate if needed
|
|
719
733
|
if positions[-1] <= 0:
|
|
720
734
|
# All stops are negative, fill with the last color
|
|
721
|
-
return 1, 'solid', None, [], [self.colors[-1]]
|
|
735
|
+
return 1, 'solid', None, [], [self.colors[-1]], []
|
|
722
736
|
for i, position in enumerate(positions):
|
|
723
737
|
if position == 0:
|
|
724
738
|
# Keep colors and positions from this rank
|
|
@@ -733,8 +747,8 @@ class RadialGradient(Gradient):
|
|
|
733
747
|
intermediate_color = gradient_average_color(
|
|
734
748
|
[previous_color, previous_color, color, color],
|
|
735
749
|
[previous_position, 0, 0, position])
|
|
736
|
-
colors = [intermediate_color
|
|
737
|
-
positions = [0
|
|
750
|
+
colors = [intermediate_color, *colors[i:]]
|
|
751
|
+
positions = [0, *positions[i:]]
|
|
738
752
|
break
|
|
739
753
|
first, last, positions = normalize_stop_positions(positions)
|
|
740
754
|
|
|
@@ -742,7 +756,7 @@ class RadialGradient(Gradient):
|
|
|
742
756
|
# See https://drafts.csswg.org/css-images-3/#repeating-gradients
|
|
743
757
|
if first == last and self.repeating:
|
|
744
758
|
color = gradient_average_color(colors, positions)
|
|
745
|
-
return 1, 'solid', None, [], [color]
|
|
759
|
+
return 1, 'solid', None, [], [color], []
|
|
746
760
|
|
|
747
761
|
# Define the coordinates of the gradient circles
|
|
748
762
|
points = (
|
|
@@ -753,7 +767,7 @@ class RadialGradient(Gradient):
|
|
|
753
767
|
points, positions, colors = self._repeat(
|
|
754
768
|
width, height, scale_y, points, positions, colors)
|
|
755
769
|
|
|
756
|
-
return scale_y, 'radial', points, positions, colors
|
|
770
|
+
return scale_y, 'radial', points, positions, colors, hints
|
|
757
771
|
|
|
758
772
|
def _repeat(self, width, height, scale_y, points, positions, colors):
|
|
759
773
|
# Keep original lists and values, they’re useful
|
|
@@ -775,7 +789,7 @@ class RadialGradient(Gradient):
|
|
|
775
789
|
colors *= repeat
|
|
776
790
|
positions = [
|
|
777
791
|
i + position for i in range(repeat) for position in positions]
|
|
778
|
-
points = points[:5]
|
|
792
|
+
points = (*points[:5], points[5] + gradient_length * repeat_after)
|
|
779
793
|
|
|
780
794
|
if points[2] == 0:
|
|
781
795
|
# Inner circle has 0 radius, no need to repeat inside, return
|
|
@@ -785,7 +799,7 @@ class RadialGradient(Gradient):
|
|
|
785
799
|
repeat_before = points[2] / gradient_length
|
|
786
800
|
|
|
787
801
|
# Set the inner circle size to 0
|
|
788
|
-
points = points[:2]
|
|
802
|
+
points = (*points[:2], 0, *points[3:])
|
|
789
803
|
|
|
790
804
|
# Find how many times the whole gradient can be repeated
|
|
791
805
|
full_repeat = int(repeat_before)
|
|
@@ -830,19 +844,19 @@ class RadialGradient(Gradient):
|
|
|
830
844
|
average_positions = [position, ratio, ratio, next_position]
|
|
831
845
|
zero_color = gradient_average_color(
|
|
832
846
|
average_colors, average_positions)
|
|
833
|
-
colors = [zero_color
|
|
847
|
+
colors = [zero_color, *original_colors[-(i - 1):], *colors]
|
|
834
848
|
new_positions = [
|
|
835
849
|
position - 1 - full_repeat for position
|
|
836
850
|
in original_positions[-(i - 1):]]
|
|
837
851
|
positions = (ratio - 1 - full_repeat, *new_positions, *positions)
|
|
838
852
|
return points, positions, colors
|
|
839
853
|
|
|
840
|
-
def _resolve_size(self, width, height, center_x, center_y):
|
|
854
|
+
def _resolve_size(self, width, height, center_x, center_y, style):
|
|
841
855
|
"""Resolve circle size of the radial gradient."""
|
|
842
856
|
if self.size_type == 'explicit':
|
|
843
857
|
size_x, size_y = self.size
|
|
844
|
-
size_x = percentage(size_x, width)
|
|
845
|
-
size_y = percentage(size_y, height)
|
|
858
|
+
size_x = percentage(size_x, style, width)
|
|
859
|
+
size_y = percentage(size_y, style, height)
|
|
846
860
|
return size_x, size_y
|
|
847
861
|
left = abs(center_x)
|
|
848
862
|
right = abs(width - center_x)
|