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
weasyprint/stacking.py
CHANGED
|
@@ -45,7 +45,7 @@ class StackingContext:
|
|
|
45
45
|
child_contexts = [cls.from_box(child, page) for child in page.children]
|
|
46
46
|
# Children are sub-contexts, remove them from the "normal" tree.
|
|
47
47
|
page = page.copy_with_children([])
|
|
48
|
-
return cls(page, child_contexts, [], [],
|
|
48
|
+
return cls(page, child_contexts, [], [], {}, page)
|
|
49
49
|
|
|
50
50
|
@classmethod
|
|
51
51
|
def from_box(cls, box, page, child_contexts=None):
|
|
@@ -60,7 +60,7 @@ class StackingContext:
|
|
|
60
60
|
# context, not this new one."
|
|
61
61
|
blocks = []
|
|
62
62
|
floats = []
|
|
63
|
-
blocks_and_cells =
|
|
63
|
+
blocks_and_cells = {}
|
|
64
64
|
box = _dispatch_children(
|
|
65
65
|
box, page, child_contexts, blocks, floats, blocks_and_cells)
|
|
66
66
|
return cls(box, children, blocks, floats, blocks_and_cells, page)
|
|
@@ -99,22 +99,21 @@ def _dispatch(box, page, child_contexts, blocks, floats, blocks_and_cells):
|
|
|
99
99
|
else:
|
|
100
100
|
if isinstance(box, boxes.BlockLevelBox):
|
|
101
101
|
blocks_index = len(blocks)
|
|
102
|
-
|
|
102
|
+
box_blocks_and_cells = {}
|
|
103
|
+
box = _dispatch_children(
|
|
104
|
+
box, page, child_contexts, blocks, floats, box_blocks_and_cells)
|
|
105
|
+
blocks.insert(blocks_index, box)
|
|
106
|
+
blocks_and_cells[box] = box_blocks_and_cells
|
|
103
107
|
elif isinstance(box, boxes.TableCellBox):
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
box_blocks_and_cells = {}
|
|
109
|
+
box = _dispatch_children(
|
|
110
|
+
box, page, child_contexts, blocks, floats, box_blocks_and_cells)
|
|
111
|
+
blocks_and_cells[box] = box_blocks_and_cells
|
|
106
112
|
else:
|
|
107
113
|
blocks_index = None
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
box, page, child_contexts, blocks, floats, blocks_and_cells)
|
|
112
|
-
|
|
113
|
-
# Insert at the positions before dispatch the children.
|
|
114
|
-
if blocks_index is not None:
|
|
115
|
-
blocks.insert(blocks_index, box)
|
|
116
|
-
if blocks_and_cells_index is not None:
|
|
117
|
-
blocks_and_cells.insert(blocks_and_cells_index, box)
|
|
114
|
+
box_blocks_and_cells = None
|
|
115
|
+
box = _dispatch_children(
|
|
116
|
+
box, page, child_contexts, blocks, floats, blocks_and_cells)
|
|
118
117
|
|
|
119
118
|
return box
|
|
120
119
|
|
weasyprint/svg/__init__.py
CHANGED
|
@@ -92,6 +92,7 @@ class Node:
|
|
|
92
92
|
self._wrapper = wrapper
|
|
93
93
|
self._etree_node = wrapper.etree_element
|
|
94
94
|
self._style = style
|
|
95
|
+
self._children = None
|
|
95
96
|
|
|
96
97
|
self.attrib = wrapper.etree_element.attrib.copy()
|
|
97
98
|
|
|
@@ -175,13 +176,16 @@ class Node:
|
|
|
175
176
|
child._wrapper.etree_children = [
|
|
176
177
|
child._etree_node for child in children]
|
|
177
178
|
|
|
178
|
-
|
|
179
179
|
def __iter__(self):
|
|
180
180
|
"""Yield node children, handling cascade."""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
self.
|
|
184
|
-
|
|
181
|
+
if self._children is None:
|
|
182
|
+
children = []
|
|
183
|
+
for wrapper in self._wrapper:
|
|
184
|
+
child = Node(wrapper, self._style)
|
|
185
|
+
self.cascade(child)
|
|
186
|
+
children.append(child)
|
|
187
|
+
self._children = children
|
|
188
|
+
return iter(self._children)
|
|
185
189
|
|
|
186
190
|
def get_viewbox(self):
|
|
187
191
|
"""Get node viewBox as a tuple of floats."""
|
|
@@ -210,7 +214,7 @@ class Node:
|
|
|
210
214
|
return re.sub('[\n\r\t]', ' ', string)
|
|
211
215
|
else:
|
|
212
216
|
string = re.sub('[\n\r]', '', string)
|
|
213
|
-
string =
|
|
217
|
+
string = string.replace('\t', ' ')
|
|
214
218
|
return re.sub(' +', ' ', string)
|
|
215
219
|
|
|
216
220
|
def get_child(self, id_):
|
|
@@ -320,14 +324,14 @@ class Node:
|
|
|
320
324
|
svg.inner_diagonal = hypot(svg.inner_width, svg.inner_height) / sqrt(2)
|
|
321
325
|
|
|
322
326
|
|
|
323
|
-
|
|
324
327
|
class SVG:
|
|
325
328
|
"""An SVG document."""
|
|
326
329
|
|
|
327
|
-
def __init__(self, tree, url):
|
|
330
|
+
def __init__(self, tree, url, font_config):
|
|
328
331
|
wrapper = ElementWrapper.from_xml_root(tree)
|
|
329
332
|
style = parse_stylesheets(wrapper, url)
|
|
330
333
|
self.tree = Node(wrapper, style)
|
|
334
|
+
self.font_config = font_config
|
|
331
335
|
self.url = url
|
|
332
336
|
self.filters = {}
|
|
333
337
|
self.gradients = {}
|
|
@@ -401,6 +405,9 @@ class SVG:
|
|
|
401
405
|
|
|
402
406
|
original_streams = []
|
|
403
407
|
|
|
408
|
+
call_fill_stroke = fill_stroke and node.tag in (
|
|
409
|
+
'circle', 'ellipse', 'line', 'path', 'polyline', 'polygon', 'rect')
|
|
410
|
+
|
|
404
411
|
if fill_stroke:
|
|
405
412
|
self.stream.push_state()
|
|
406
413
|
|
|
@@ -410,16 +417,17 @@ class SVG:
|
|
|
410
417
|
apply_filters(self, node, filter_, font_size)
|
|
411
418
|
|
|
412
419
|
# Apply transform attribute
|
|
413
|
-
self.transform(node
|
|
420
|
+
self.transform(node, font_size)
|
|
414
421
|
|
|
415
422
|
# Create substream for opacity
|
|
416
423
|
opacity = alpha_value(node.get('opacity', 1))
|
|
417
424
|
if fill_stroke and 0 <= opacity < 1:
|
|
418
425
|
original_streams.append(self.stream)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
426
|
+
self.stream = self.stream.add_group(0, 0, 0, 0) # BBox set after drawing
|
|
427
|
+
|
|
428
|
+
# Set graphical state
|
|
429
|
+
if call_fill_stroke:
|
|
430
|
+
self.set_graphical_state(node, font_size)
|
|
423
431
|
|
|
424
432
|
# Clip
|
|
425
433
|
clip_path = parse_url(node.get('clip-path')).fragment
|
|
@@ -515,7 +523,7 @@ class SVG:
|
|
|
515
523
|
paint_mask(self, node, mask, opacity)
|
|
516
524
|
|
|
517
525
|
# Fill and stroke
|
|
518
|
-
if
|
|
526
|
+
if call_fill_stroke:
|
|
519
527
|
self.fill_stroke(node, font_size)
|
|
520
528
|
|
|
521
529
|
# Draw markers
|
|
@@ -523,6 +531,12 @@ class SVG:
|
|
|
523
531
|
|
|
524
532
|
# Apply opacity stream and restore original stream
|
|
525
533
|
if fill_stroke and 0 <= opacity < 1:
|
|
534
|
+
box = self.calculate_bounding_box(node, font_size)
|
|
535
|
+
if not is_valid_bounding_box(box):
|
|
536
|
+
box = (0, 0, self.inner_width, self.inner_height)
|
|
537
|
+
x, y, width, height = box
|
|
538
|
+
self.stream.extra['BBox'][:] = x, y, x + width, y + height
|
|
539
|
+
|
|
526
540
|
group_id = self.stream.id
|
|
527
541
|
self.stream = original_streams.pop()
|
|
528
542
|
self.stream.set_alpha(opacity, stroke=True, fill=True)
|
|
@@ -665,37 +679,30 @@ class SVG:
|
|
|
665
679
|
|
|
666
680
|
return source, color
|
|
667
681
|
|
|
668
|
-
def
|
|
669
|
-
"""
|
|
670
|
-
if node.tag in ('text', 'textPath', 'a') and not text:
|
|
671
|
-
return
|
|
672
|
-
|
|
682
|
+
def set_graphical_state(self, node, font_size, text=False):
|
|
683
|
+
"""Set stroke and fill colors, and line options."""
|
|
673
684
|
# Get fill data
|
|
674
685
|
fill_source, fill_color = self.get_paint(node.get('fill', 'black'))
|
|
675
686
|
fill_opacity = alpha_value(node.get('fill-opacity', 1))
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if fill_color and not
|
|
687
|
+
fill_in_gradient = fill_source in self.gradients
|
|
688
|
+
fill_in_pattern = fill_source in self.patterns
|
|
689
|
+
if fill_color and not (fill_in_gradient or fill_in_pattern):
|
|
679
690
|
stream_color = color(fill_color)
|
|
680
691
|
stream_color.alpha *= fill_opacity
|
|
681
692
|
self.stream.set_color(stream_color)
|
|
682
|
-
fill = fill_color or fill_drawn
|
|
683
693
|
|
|
684
694
|
# Get stroke data
|
|
685
695
|
stroke_source, stroke_color = self.get_paint(node.get('stroke'))
|
|
686
696
|
stroke_opacity = alpha_value(node.get('stroke-opacity', 1))
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if stroke_color and not
|
|
697
|
+
stroke_in_gradient = stroke_source in self.gradients
|
|
698
|
+
stroke_in_pattern = stroke_source in self.patterns
|
|
699
|
+
if stroke_color and not (stroke_in_gradient or stroke_in_pattern):
|
|
690
700
|
stream_color = color(stroke_color)
|
|
691
701
|
stream_color.alpha *= stroke_opacity
|
|
692
702
|
self.stream.set_color(stream_color, stroke=True)
|
|
693
|
-
stroke = stroke_color or stroke_drawn
|
|
694
703
|
stroke_width = self.length(node.get('stroke-width', '1px'), font_size)
|
|
695
704
|
if stroke_width:
|
|
696
705
|
self.stream.set_line_width(stroke_width)
|
|
697
|
-
else:
|
|
698
|
-
stroke = None
|
|
699
706
|
|
|
700
707
|
# Apply dash array
|
|
701
708
|
dash_array = tuple(
|
|
@@ -738,6 +745,23 @@ class SVG:
|
|
|
738
745
|
miter_limit = 4
|
|
739
746
|
self.stream.set_miter_limit(miter_limit)
|
|
740
747
|
|
|
748
|
+
def fill_stroke(self, node, font_size, text=False):
|
|
749
|
+
"""Paint fill and stroke for a node."""
|
|
750
|
+
# Get fill data
|
|
751
|
+
fill_source, fill_color = self.get_paint(node.get('fill', 'black'))
|
|
752
|
+
fill_opacity = alpha_value(node.get('fill-opacity', 1))
|
|
753
|
+
fill_drawn = draw_gradient_or_pattern(
|
|
754
|
+
self, node, fill_source, font_size, fill_opacity, stroke=False)
|
|
755
|
+
fill = fill_color or fill_drawn
|
|
756
|
+
|
|
757
|
+
# Get stroke data
|
|
758
|
+
stroke_source, stroke_color = self.get_paint(node.get('stroke'))
|
|
759
|
+
stroke_opacity = alpha_value(node.get('stroke-opacity', 1))
|
|
760
|
+
stroke_drawn = draw_gradient_or_pattern(
|
|
761
|
+
self, node, stroke_source, font_size, stroke_opacity, stroke=True)
|
|
762
|
+
stroke_width = self.length(node.get('stroke-width', '1px'), font_size)
|
|
763
|
+
stroke = (stroke_color or stroke_drawn) and stroke_width
|
|
764
|
+
|
|
741
765
|
# Fill and stroke
|
|
742
766
|
even_odd = node.get('fill-rule') == 'evenodd'
|
|
743
767
|
if text:
|
|
@@ -760,12 +784,15 @@ class SVG:
|
|
|
760
784
|
else:
|
|
761
785
|
self.stream.end()
|
|
762
786
|
|
|
763
|
-
def transform(self,
|
|
787
|
+
def transform(self, node, font_size):
|
|
764
788
|
"""Apply a transformation string to the node."""
|
|
789
|
+
transform_origin = node.get('transform-origin')
|
|
790
|
+
transform_string = node.get('transform')
|
|
765
791
|
if not transform_string:
|
|
766
792
|
return
|
|
767
793
|
|
|
768
|
-
matrix = transform(
|
|
794
|
+
matrix = transform(
|
|
795
|
+
transform_string, transform_origin, font_size, self.inner_diagonal)
|
|
769
796
|
if matrix.determinant:
|
|
770
797
|
self.stream.transform(*matrix.values)
|
|
771
798
|
|
|
@@ -813,7 +840,7 @@ class SVG:
|
|
|
813
840
|
class Pattern(SVG):
|
|
814
841
|
"""SVG node applied as a pattern."""
|
|
815
842
|
def __init__(self, tree, svg):
|
|
816
|
-
super().__init__(tree._etree_node, svg.url)
|
|
843
|
+
super().__init__(tree._etree_node, svg.url, svg.font_config)
|
|
817
844
|
self.svg = svg
|
|
818
845
|
self.tree = tree
|
|
819
846
|
|
weasyprint/svg/bounding_box.py
CHANGED
|
@@ -206,7 +206,7 @@ def bounding_box_path(svg, node, font_size):
|
|
|
206
206
|
|
|
207
207
|
def bounding_box_text(svg, node, font_size):
|
|
208
208
|
"""Bounding box for text node."""
|
|
209
|
-
return node
|
|
209
|
+
return getattr(node, 'text_bounding_box', None)
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
def bounding_box_g(svg, node, font_size):
|
|
@@ -229,7 +229,9 @@ def bounding_box_use(svg, node, font_size):
|
|
|
229
229
|
if (tree := get_use_tree(svg, node, font_size)) is None:
|
|
230
230
|
return EMPTY_BOUNDING_BOX
|
|
231
231
|
else:
|
|
232
|
-
|
|
232
|
+
x, y = svg.point(node.get('x'), node.get('y'), font_size)
|
|
233
|
+
box = bounding_box(svg, tree, font_size, True)
|
|
234
|
+
return box[0] + x, box[1] + y, box[2], box[3]
|
|
233
235
|
|
|
234
236
|
|
|
235
237
|
def _bounding_box_elliptical_arc(x1, y1, rx, ry, phi, large, sweep, x, y):
|
weasyprint/svg/defs.py
CHANGED
|
@@ -44,12 +44,6 @@ def get_use_tree(svg, node, font_size):
|
|
|
44
44
|
|
|
45
45
|
def use(svg, node, font_size):
|
|
46
46
|
"""Draw use tags."""
|
|
47
|
-
x, y = svg.point(node.get('x'), node.get('y'), font_size)
|
|
48
|
-
|
|
49
|
-
for attribute in ('x', 'y', 'viewBox', 'mask'):
|
|
50
|
-
if attribute in node.attrib:
|
|
51
|
-
del node.attrib[attribute]
|
|
52
|
-
|
|
53
47
|
if (tree := get_use_tree(svg, node, font_size)) is None:
|
|
54
48
|
return
|
|
55
49
|
|
|
@@ -69,6 +63,7 @@ def use(svg, node, font_size):
|
|
|
69
63
|
|
|
70
64
|
node.cascade(tree)
|
|
71
65
|
node.override_iter(iter((tree,)))
|
|
66
|
+
x, y = svg.point(node.get('x'), node.get('y'), font_size)
|
|
72
67
|
svg.stream.transform(e=x, f=y)
|
|
73
68
|
|
|
74
69
|
|
|
@@ -137,7 +132,7 @@ def draw_gradient(svg, node, gradient, font_size, opacity, stroke):
|
|
|
137
132
|
|
|
138
133
|
if 'gradientTransform' in gradient.attrib:
|
|
139
134
|
transform_matrix = transform(
|
|
140
|
-
gradient.get('gradientTransform'), font_size,
|
|
135
|
+
gradient.get('gradientTransform'), '0 0', font_size,
|
|
141
136
|
svg.normalized_diagonal)
|
|
142
137
|
matrix = transform_matrix @ matrix
|
|
143
138
|
|
|
@@ -407,7 +402,7 @@ def spread_radial_gradient(spread, positions, colors, fx, fy, fr, cx, cy, r,
|
|
|
407
402
|
average_positions = [position, ratio, ratio, next_position]
|
|
408
403
|
zero_color = gradient_average_color(
|
|
409
404
|
average_colors, average_positions)
|
|
410
|
-
colors = [zero_color
|
|
405
|
+
colors = [zero_color, *original_colors[-(i - 1):], *colors]
|
|
411
406
|
new_positions = [
|
|
412
407
|
position - 1 - full_repeat for position
|
|
413
408
|
in original_positions[-(i - 1):]]
|
|
@@ -450,7 +445,7 @@ def draw_pattern(svg, node, pattern, font_size, opacity, stroke):
|
|
|
450
445
|
|
|
451
446
|
if 'patternTransform' in pattern.attrib:
|
|
452
447
|
transform_matrix = transform(
|
|
453
|
-
pattern.get('patternTransform'), font_size, svg.inner_diagonal)
|
|
448
|
+
pattern.get('patternTransform'), '0 0', font_size, svg.inner_diagonal)
|
|
454
449
|
matrix = transform_matrix @ matrix
|
|
455
450
|
|
|
456
451
|
matrix = matrix @ svg.stream.ctm
|
weasyprint/svg/images.py
CHANGED
|
@@ -58,8 +58,15 @@ def image(svg, node, font_size):
|
|
|
58
58
|
intrinsic_width = intrinsic_ratio * intrinsic_height
|
|
59
59
|
elif intrinsic_height is None:
|
|
60
60
|
intrinsic_height = intrinsic_width / intrinsic_ratio
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
|
|
62
|
+
# Calculate final dimensions while preserving aspect ratio
|
|
63
|
+
if width and not height:
|
|
64
|
+
height = width / intrinsic_ratio
|
|
65
|
+
elif height and not width:
|
|
66
|
+
width = height * intrinsic_ratio
|
|
67
|
+
else:
|
|
68
|
+
width = width or intrinsic_width
|
|
69
|
+
height = height or intrinsic_height
|
|
63
70
|
|
|
64
71
|
scale_x, scale_y, translate_x, translate_y = preserve_ratio(
|
|
65
72
|
svg, node, font_size, width, height,
|
|
@@ -69,8 +76,9 @@ def image(svg, node, font_size):
|
|
|
69
76
|
svg.stream.end()
|
|
70
77
|
svg.stream.push_state()
|
|
71
78
|
svg.stream.transform(a=scale_x, d=scale_y, e=translate_x, f=translate_y)
|
|
79
|
+
# TODO: pass real style instead of dict.
|
|
72
80
|
image.draw(
|
|
73
81
|
svg.stream, intrinsic_width, intrinsic_height,
|
|
74
|
-
image_rendering
|
|
82
|
+
{'image_rendering': node.attrib.get('image-rendering', 'auto')},
|
|
75
83
|
)
|
|
76
84
|
svg.stream.pop_state()
|
weasyprint/svg/text.py
CHANGED
|
@@ -18,6 +18,10 @@ class TextBox:
|
|
|
18
18
|
return self.pango_layout.text
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
class Style(dict):
|
|
22
|
+
"""Dummy class to store dict."""
|
|
23
|
+
|
|
24
|
+
|
|
21
25
|
def text(svg, node, font_size):
|
|
22
26
|
"""Draw text node."""
|
|
23
27
|
from ..css.properties import INITIAL_VALUES
|
|
@@ -25,7 +29,9 @@ def text(svg, node, font_size):
|
|
|
25
29
|
from ..text.line_break import split_first_line
|
|
26
30
|
|
|
27
31
|
# TODO: use real computed values
|
|
28
|
-
style =
|
|
32
|
+
style = Style()
|
|
33
|
+
style.update(INITIAL_VALUES)
|
|
34
|
+
style.font_config = svg.font_config
|
|
29
35
|
style['font_family'] = [
|
|
30
36
|
font.strip('"\'') for font in
|
|
31
37
|
node.get('font-family', 'sans-serif').split(',')]
|
|
@@ -123,6 +129,7 @@ def text(svg, node, font_size):
|
|
|
123
129
|
return
|
|
124
130
|
|
|
125
131
|
svg.stream.push_state()
|
|
132
|
+
svg.set_graphical_state(node, font_size, text=True)
|
|
126
133
|
svg.stream.begin_text()
|
|
127
134
|
emoji_lines = []
|
|
128
135
|
|
|
@@ -166,4 +173,6 @@ def text(svg, node, font_size):
|
|
|
166
173
|
svg.stream.pop_state()
|
|
167
174
|
|
|
168
175
|
for font_size, x, y, emojis in emoji_lines:
|
|
169
|
-
|
|
176
|
+
# TODO: pass real style instead of dict.
|
|
177
|
+
style = {'font_size': font_size}
|
|
178
|
+
draw_emojis(svg.stream, style, x, y, emojis)
|
weasyprint/svg/utils.py
CHANGED
|
@@ -5,7 +5,7 @@ from contextlib import suppress
|
|
|
5
5
|
from math import cos, radians, sin, tan
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
7
|
|
|
8
|
-
from tinycss2.
|
|
8
|
+
from tinycss2.color5 import parse_color
|
|
9
9
|
|
|
10
10
|
from ..matrix import Matrix
|
|
11
11
|
|
|
@@ -25,7 +25,7 @@ def normalize(string):
|
|
|
25
25
|
|
|
26
26
|
def size(string, font_size=None, percentage_reference=None):
|
|
27
27
|
"""Compute size from string, resolving units and percentages."""
|
|
28
|
-
from ..css.
|
|
28
|
+
from ..css.units import LENGTHS_TO_PIXELS
|
|
29
29
|
|
|
30
30
|
if not string:
|
|
31
31
|
return 0
|
|
@@ -94,7 +94,10 @@ def preserve_ratio(svg, node, font_size, width, height, viewbox=None):
|
|
|
94
94
|
scale_x = width / viewbox_width if viewbox_width else 1
|
|
95
95
|
scale_y = height / viewbox_height if viewbox_height else 1
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
if viewbox:
|
|
98
|
+
aspect_ratio = node.get('preserveAspectRatio', 'xMidYMid').split()
|
|
99
|
+
else:
|
|
100
|
+
aspect_ratio = ('none',)
|
|
98
101
|
align = aspect_ratio[0]
|
|
99
102
|
if align == 'none':
|
|
100
103
|
x_position = 'min'
|
|
@@ -149,13 +152,17 @@ def color(string):
|
|
|
149
152
|
return parse_color(string or '') or parse_color('black')
|
|
150
153
|
|
|
151
154
|
|
|
152
|
-
def transform(transform_string, font_size, normalized_diagonal):
|
|
155
|
+
def transform(transform_string, transform_origin, font_size, normalized_diagonal):
|
|
153
156
|
"""Get a matrix corresponding to the transform string."""
|
|
154
157
|
# TODO: merge with gather_anchors and css.validation.properties.transform
|
|
155
|
-
transformations = re.findall(
|
|
156
|
-
r'(\w+) ?\( ?(.*?) ?\)', normalize(transform_string))
|
|
157
|
-
matrix = Matrix()
|
|
158
158
|
|
|
159
|
+
origin_x, origin_y = 0, 0
|
|
160
|
+
size_strings = normalize(transform_origin).split()
|
|
161
|
+
if len(size_strings) == 2:
|
|
162
|
+
origin_x, origin_y = size(size_strings[0]), size(size_strings[1])
|
|
163
|
+
matrix = Matrix(e=origin_x, f=origin_y)
|
|
164
|
+
|
|
165
|
+
transformations = re.findall(r'(\w+) ?\( ?(.*?) ?\)', normalize(transform_string))
|
|
159
166
|
for transformation_type, transformation in transformations:
|
|
160
167
|
values = [
|
|
161
168
|
size(value, font_size, normalized_diagonal)
|
|
@@ -196,4 +203,4 @@ def transform(transform_string, font_size, normalized_diagonal):
|
|
|
196
203
|
if transformation_type in ('scaleY', 'scale'):
|
|
197
204
|
matrix = Matrix(d=values.pop(0)) @ matrix
|
|
198
205
|
|
|
199
|
-
return matrix
|
|
206
|
+
return Matrix(e=-origin_x, f=-origin_y) @ matrix
|
weasyprint/text/constants.py
CHANGED
weasyprint/text/ffi.py
CHANGED
|
@@ -22,6 +22,7 @@ ffi.cdef('''
|
|
|
22
22
|
hb_blob_t * hb_face_reference_blob (hb_face_t *face);
|
|
23
23
|
unsigned int hb_face_get_index (const hb_face_t *face);
|
|
24
24
|
unsigned int hb_face_get_upem (const hb_face_t *face);
|
|
25
|
+
unsigned int hb_face_get_glyph_count (const hb_face_t *face);
|
|
25
26
|
hb_blob_t * hb_face_reference_table (const hb_face_t *face, hb_tag_t tag);
|
|
26
27
|
const char * hb_blob_get_data (hb_blob_t *blob, unsigned int *length);
|
|
27
28
|
unsigned int hb_blob_get_length (hb_blob_t *blob);
|
|
@@ -450,8 +451,8 @@ def _dlopen(ffi, *names, allow_fail=False):
|
|
|
450
451
|
return ffi.dlopen(name, flags)
|
|
451
452
|
if allow_fail:
|
|
452
453
|
return
|
|
453
|
-
#
|
|
454
|
-
print(
|
|
454
|
+
# Print error message and re-raise the exception.
|
|
455
|
+
print( # noqa: T201, logger is not configured yet
|
|
455
456
|
'\n-----\n\n'
|
|
456
457
|
'WeasyPrint could not import some external libraries. Please '
|
|
457
458
|
'carefully follow the installation steps before reporting an issue:\n'
|
|
@@ -474,7 +475,7 @@ if hasattr(os, 'add_dll_directory') and not hasattr(sys, 'frozen'): # pragma: n
|
|
|
474
475
|
|
|
475
476
|
gobject = _dlopen(
|
|
476
477
|
ffi, 'libgobject-2.0-0', 'gobject-2.0-0', 'gobject-2.0',
|
|
477
|
-
'libgobject-2.0.so.0', 'libgobject-2.0.dylib', 'libgobject-2.0-0.dll')
|
|
478
|
+
'libgobject-2.0.so.0', 'libgobject-2.0.0.dylib', 'libgobject-2.0-0.dll')
|
|
478
479
|
pango = _dlopen(
|
|
479
480
|
ffi, 'libpango-1.0-0', 'pango-1.0-0', 'pango-1.0', 'libpango-1.0.so.0',
|
|
480
481
|
'libpango-1.0.dylib', 'libpango-1.0-0.dll')
|
weasyprint/text/fonts.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from hashlib import md5
|
|
4
4
|
from io import BytesIO
|
|
5
|
+
from locale import getpreferredencoding
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from shutil import rmtree
|
|
7
8
|
from tempfile import mkdtemp
|
|
@@ -11,7 +12,7 @@ from xml.etree.ElementTree import Element, SubElement, tostring
|
|
|
11
12
|
from fontTools.ttLib import TTFont, woff2
|
|
12
13
|
|
|
13
14
|
from ..logger import LOGGER
|
|
14
|
-
from ..urls import
|
|
15
|
+
from ..urls import fetch
|
|
15
16
|
|
|
16
17
|
from .constants import ( # isort:skip
|
|
17
18
|
CAPS_KEYS, EAST_ASIAN_KEYS, FONTCONFIG_STRETCH, FONTCONFIG_STYLE, FONTCONFIG_WEIGHT,
|
|
@@ -20,6 +21,8 @@ from .ffi import ( # isort:skip
|
|
|
20
21
|
FROM_UNITS, TO_UNITS, ffi, fontconfig, gobject, harfbuzz, pango, pangoft2,
|
|
21
22
|
unicode_to_char_p)
|
|
22
23
|
|
|
24
|
+
PREFERRED_ENCODING = getpreferredencoding(False)
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
def _check_font_configuration(font_config): # pragma: no cover
|
|
25
28
|
"""Check whether the given font_config has fonts.
|
|
@@ -105,6 +108,10 @@ class FontConfiguration:
|
|
|
105
108
|
# Temporary folder storing fonts.
|
|
106
109
|
self._folder = None
|
|
107
110
|
|
|
111
|
+
# Cache.
|
|
112
|
+
self.strut_layouts = {}
|
|
113
|
+
self.font_features = {}
|
|
114
|
+
|
|
108
115
|
def add_font_face(self, rule_descriptors, url_fetcher):
|
|
109
116
|
"""Add a font face to the Fontconfig configuration."""
|
|
110
117
|
|
|
@@ -150,7 +157,7 @@ class FontConfiguration:
|
|
|
150
157
|
if font_name.lower() == name.lower():
|
|
151
158
|
fontconfig.FcPatternGetString(
|
|
152
159
|
matching_pattern, b'file', 0, string)
|
|
153
|
-
path = ffi.string(string[0]).decode(
|
|
160
|
+
path = ffi.string(string[0]).decode(PREFERRED_ENCODING)
|
|
154
161
|
url = Path(path).as_uri()
|
|
155
162
|
break
|
|
156
163
|
else:
|
|
@@ -191,7 +198,8 @@ class FontConfiguration:
|
|
|
191
198
|
match = SubElement(root, 'match', target='scan')
|
|
192
199
|
test = SubElement(match, 'test', name='file', compare='eq')
|
|
193
200
|
SubElement(test, 'string').text = str(font_path)
|
|
194
|
-
|
|
201
|
+
# Prepend, as replacing the font family breaks Pango, see #2510.
|
|
202
|
+
edit = SubElement(match, 'edit', name='family', mode='prepend')
|
|
195
203
|
SubElement(edit, 'string').text = rule_descriptors['font_family']
|
|
196
204
|
if 'font_style' in rule_descriptors:
|
|
197
205
|
edit = SubElement(match, 'edit', name='slant', mode=mode)
|
|
@@ -235,7 +243,7 @@ class FontConfiguration:
|
|
|
235
243
|
# too as explained in Behdad's blog entry.
|
|
236
244
|
fontconfig.FcConfigParseAndLoadFromMemory(self._config, xml, True)
|
|
237
245
|
font_added = fontconfig.FcConfigAppFontAddFile(
|
|
238
|
-
self._config, str(font_path).encode(
|
|
246
|
+
self._config, str(font_path).encode(PREFERRED_ENCODING))
|
|
239
247
|
if font_added:
|
|
240
248
|
return pangoft2.pango_fc_font_map_config_changed(
|
|
241
249
|
ffi.cast('PangoFcFontMap *', self.font_map))
|
|
@@ -374,7 +382,7 @@ def get_pango_font_key(pango_font):
|
|
|
374
382
|
# TODO: This value is stable for a given Pango font in a given Pango map, but can’t
|
|
375
383
|
# be cached with just the Pango font as a key because two Pango fonts could point to
|
|
376
384
|
# the same address for two different Pango maps. We should cache it in the
|
|
377
|
-
# FontConfiguration object. See
|
|
385
|
+
# FontConfiguration object. See issue #2144.
|
|
378
386
|
description = ffi.gc(
|
|
379
387
|
pango.pango_font_describe(pango_font), pango.pango_font_description_free)
|
|
380
388
|
font_size = pango.pango_font_description_get_size(description) * FROM_UNITS
|