weasyprint 67.0__py3-none-any.whl → 68.1__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.
@@ -11,6 +11,7 @@ from .. import __version__
11
11
  NS = {
12
12
  'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
13
13
  'dc': 'http://purl.org/dc/elements/1.1/',
14
+ '': '',
14
15
  'xmp': 'http://ns.adobe.com/xap/1.0/',
15
16
  'xmpMM': 'http://ns.adobe.com/xap/1.0/mm/',
16
17
  'pdf': 'http://ns.adobe.com/pdf/1.3/',
@@ -23,110 +24,164 @@ for key, value in NS.items():
23
24
  register_namespace(key, value)
24
25
 
25
26
 
26
- def add_metadata(pdf, metadata, variant, version, conformance, compress):
27
- """Add PDF stream of metadata.
28
-
29
- Described in ISO-32000-1:2008, 14.3.2.
27
+ class DocumentMetadata:
28
+ """Meta-information belonging to a whole :class:`Document`.
30
29
 
30
+ New attributes may be added in future versions of WeasyPrint.
31
31
  """
32
- header = b'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>'
33
- footer = b'<?xpacket end="r"?>'
34
- xml_data = metadata.generate_rdf_metadata(metadata, variant, version, conformance)
35
- stream_content = b'\n'.join((header, xml_data, footer))
36
- extra = {'Type': '/Metadata', 'Subtype': '/XML'}
37
- metadata = pydyf.Stream([stream_content], extra, compress)
38
- pdf.add_object(metadata)
39
- pdf.catalog['Metadata'] = metadata.reference
32
+ def __init__(self, title=None, authors=None, description=None, keywords=None,
33
+ generator=None, created=None, modified=None, attachments=None,
34
+ lang=None, custom=None, xmp_metadata=None):
35
+ #: The title of the document, as a string or :obj:`None`.
36
+ #: Extracted from the ``<title>`` element in HTML
37
+ #: and written to the ``/Title`` info field in PDF.
38
+ self.title = title
39
+ #: The authors of the document, as a list of strings.
40
+ #: (Defaults to the empty list.)
41
+ #: Extracted from the ``<meta name=author>`` elements in HTML
42
+ #: and written to the ``/Author`` info field in PDF.
43
+ self.authors = authors or []
44
+ #: The description of the document, as a string or :obj:`None`.
45
+ #: Extracted from the ``<meta name=description>`` element in HTML
46
+ #: and written to the ``/Subject`` info field in PDF.
47
+ self.description = description
48
+ #: Keywords associated with the document, as a list of strings.
49
+ #: (Defaults to the empty list.)
50
+ #: Extracted from ``<meta name=keywords>`` elements in HTML
51
+ #: and written to the ``/Keywords`` info field in PDF.
52
+ self.keywords = keywords or []
53
+ #: The name of one of the software packages
54
+ #: used to generate the document, as a string or :obj:`None`.
55
+ #: Extracted from the ``<meta name=generator>`` element in HTML
56
+ #: and written to the ``/Creator`` info field in PDF.
57
+ self.generator = generator
58
+ #: The creation date of the document, as a string or :obj:`None`.
59
+ #: Dates are in one of the six formats specified in
60
+ #: `W3C’s profile of ISO 8601 <https://www.w3.org/TR/NOTE-datetime>`_.
61
+ #: Extracted from the ``<meta name=dcterms.created>`` element in HTML
62
+ #: and written to the ``/CreationDate`` info field in PDF.
63
+ self.created = created
64
+ #: The modification date of the document, as a string or :obj:`None`.
65
+ #: Dates are in one of the six formats specified in
66
+ #: `W3C’s profile of ISO 8601 <https://www.w3.org/TR/NOTE-datetime>`_.
67
+ #: Extracted from the ``<meta name=dcterms.modified>`` element in HTML
68
+ #: and written to the ``/ModDate`` info field in PDF.
69
+ self.modified = modified
70
+ #: A list of :class:`attachments <weasyprint.Attachment>`, empty by default.
71
+ #: Extracted from the ``<link rel=attachment>`` elements in HTML
72
+ #: and written to the ``/EmbeddedFiles`` dictionary in PDF.
73
+ self.attachments = attachments or []
74
+ #: Document language as BCP 47 language tags.
75
+ #: Extracted from ``<html lang=lang>`` in HTML.
76
+ self.lang = lang
77
+ #: Custom metadata, as a dict whose keys are the metadata names and
78
+ #: values are the metadata values.
79
+ self.custom = custom or {}
80
+ #: A list of XML bytestrings to add into the XMP metadata.
81
+ self.xmp_metadata = xmp_metadata or []
40
82
 
41
83
 
42
- def generate_rdf_metadata(metadata, variant, version, conformance):
43
- """Generate RDF metadata as a bytestring.
84
+ def include_in_pdf(self, pdf, variant, version, conformance, compress):
85
+ """Add PDF stream of metadata.
44
86
 
45
- Might be replaced by DocumentMetadata.rdf_metadata_generator().
87
+ Described in ISO-32000-1:2008, 14.3.2.
46
88
 
47
- """
48
- namespace = f'pdf{variant}id'
49
- rdf = Element(f'{{{NS["rdf"]}}}RDF')
89
+ """
90
+ header = b'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>\n'
91
+ header += b'<x:xmpmeta xmlns:x="adobe:ns:meta/">'
92
+ footer = b'</x:xmpmeta>\n<?xpacket end="r"?>'
93
+ xml_data = self.generate_rdf_metadata(variant, version, conformance)
94
+ stream_content = b'\n'.join((header, xml_data, *self.xmp_metadata, footer))
95
+ extra = {'Type': '/Metadata', 'Subtype': '/XML'}
96
+ metadata = pydyf.Stream([stream_content], extra, compress)
97
+ pdf.add_object(metadata)
98
+ pdf.catalog['Metadata'] = metadata.reference
99
+
100
+
101
+ def generate_rdf_metadata(self, variant, version, conformance):
102
+ """Generate RDF metadata as a bytestring."""
103
+ namespace = f'pdf{variant}id'
104
+ rdf = Element(f'{{{NS["rdf"]}}}RDF')
105
+
106
+ if version:
107
+ element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
108
+ element.attrib[f'{{{NS["rdf"]}}}about'] = ''
109
+ element.attrib[f'{{{NS[namespace]}}}part'] = str(version)
110
+ if conformance:
111
+ assert version
112
+ if variant == 'x':
113
+ for key in (
114
+ f'{{{NS["pdfxid"]}}}GTS_PDFXVersion',
115
+ f'{{{NS["pdfx"]}}}GTS_PDFXVersion',
116
+ f'{{{NS["pdfx"]}}}GTS_PDFXConformance',
117
+ ):
118
+ subelement = SubElement(element, key)
119
+ subelement.text = conformance
120
+ subelement = SubElement(element, f'{{{NS["pdf"]}}}Trapped')
121
+ subelement.text = 'False'
122
+ if version >= 4:
123
+ # TODO: these values could be useful instead of using random values.
124
+ assert self.modified
125
+ subelement = SubElement(element, f'{{{NS["xmp"]}}}MetadataDate')
126
+ subelement.text = self.modified
127
+ subelement = SubElement(element, f'{{{NS["xmpMM"]}}}DocumentID')
128
+ subelement.text = f'xmp.did:{uuid4()}'
129
+ subelement = SubElement(element, f'{{{NS["xmpMM"]}}}RenditionClass')
130
+ subelement.text = 'proof:pdf'
131
+ subelement = SubElement(element, f'{{{NS["xmpMM"]}}}VersionID')
132
+ subelement.text = '1'
133
+ else:
134
+ element.attrib[f'{{{NS[namespace]}}}conformance'] = conformance
135
+ if variant == 'a' and version == 4:
136
+ subelement = SubElement(element, f'{{{NS["pdfaid"]}}}rev')
137
+ subelement.text = '2020'
50
138
 
51
- if version:
52
- element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
53
- element.attrib[f'{{{NS["rdf"]}}}about'] = ''
54
- element.attrib[f'{{{NS[namespace]}}}part'] = str(version)
55
- if conformance:
56
- assert version
57
- if variant == 'x':
58
- for key in (
59
- f'{{{NS["pdfxid"]}}}GTS_PDFXVersion',
60
- f'{{{NS["pdfx"]}}}GTS_PDFXVersion',
61
- f'{{{NS["pdfx"]}}}GTS_PDFXConformance',
62
- ):
63
- subelement = SubElement(element, key)
64
- subelement.text = conformance
65
- subelement = SubElement(element, f'{{{NS["pdf"]}}}Trapped')
66
- subelement.text = 'False'
67
- if version >= 4:
68
- # TODO: these values could be useful instead of using random values.
69
- assert metadata.modified
70
- subelement = SubElement(element, f'{{{NS["xmp"]}}}MetadataDate')
71
- subelement.text = metadata.modified
72
- subelement = SubElement(element, f'{{{NS["xmpMM"]}}}DocumentID')
73
- subelement.text = f'xmp.did:{uuid4()}'
74
- subelement = SubElement(element, f'{{{NS["xmpMM"]}}}RenditionClass')
75
- subelement.text = 'proof:pdf'
76
- subelement = SubElement(element, f'{{{NS["xmpMM"]}}}VersionID')
77
- subelement.text = '1'
78
- else:
79
- element.attrib[f'{{{NS[namespace]}}}conformance'] = conformance
80
- if variant == 'a' and version == 4:
81
- subelement = SubElement(element, f'{{{NS["pdfaid"]}}}rev')
82
- subelement.text = '2020'
83
-
84
- element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
85
- element.attrib[f'{{{NS["rdf"]}}}about'] = ''
86
- element.attrib[f'{{{NS["pdf"]}}}Producer'] = f'WeasyPrint {__version__}'
87
-
88
- if metadata.title:
89
- element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
90
- element.attrib[f'{{{NS["rdf"]}}}about'] = ''
91
- element = SubElement(element, f'{{{NS["dc"]}}}title')
92
- element = SubElement(element, f'{{{NS["rdf"]}}}Alt')
93
- element = SubElement(element, f'{{{NS["rdf"]}}}li')
94
- element.attrib['xml:lang'] = 'x-default'
95
- element.text = metadata.title
96
- if metadata.authors:
97
- element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
98
- element.attrib[f'{{{NS["rdf"]}}}about'] = ''
99
- element = SubElement(element, f'{{{NS["dc"]}}}creator')
100
- element = SubElement(element, f'{{{NS["rdf"]}}}Seq')
101
- for author in metadata.authors:
102
- author_element = SubElement(element, f'{{{NS["rdf"]}}}li')
103
- author_element.text = author
104
- if metadata.description:
105
- element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
106
- element.attrib[f'{{{NS["rdf"]}}}about'] = ''
107
- element = SubElement(element, f'{{{NS["dc"]}}}subject')
108
- element = SubElement(element, f'{{{NS["rdf"]}}}Bag')
109
- element = SubElement(element, f'{{{NS["rdf"]}}}li')
110
- element.attrib['xml:lang'] = 'x-default'
111
- element.text = metadata.description
112
- if metadata.keywords:
113
- element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
114
- element.attrib[f'{{{NS["rdf"]}}}about'] = ''
115
- element = SubElement(element, f'{{{NS["pdf"]}}}Keywords')
116
- element.text = ', '.join(metadata.keywords)
117
- if metadata.generator:
118
- element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
119
- element.attrib[f'{{{NS["rdf"]}}}about'] = ''
120
- element = SubElement(element, f'{{{NS["xmp"]}}}CreatorTool')
121
- element.text = metadata.generator
122
- if metadata.created:
123
- element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
124
- element.attrib[f'{{{NS["rdf"]}}}about'] = ''
125
- element = SubElement(element, f'{{{NS["xmp"]}}}CreateDate')
126
- element.text = metadata.created
127
- if metadata.modified:
128
139
  element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
129
140
  element.attrib[f'{{{NS["rdf"]}}}about'] = ''
130
- element = SubElement(element, f'{{{NS["xmp"]}}}ModifyDate')
131
- element.text = metadata.modified
132
- return tostring(rdf, encoding='utf-8')
141
+ element.attrib[f'{{{NS["pdf"]}}}Producer'] = f'WeasyPrint {__version__}'
142
+
143
+ if self.title:
144
+ element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
145
+ element.attrib[f'{{{NS["rdf"]}}}about'] = ''
146
+ element = SubElement(element, f'{{{NS["dc"]}}}title')
147
+ element = SubElement(element, f'{{{NS["rdf"]}}}Alt')
148
+ element = SubElement(element, f'{{{NS["rdf"]}}}li')
149
+ element.attrib['xml:lang'] = 'x-default'
150
+ element.text = self.title
151
+ if self.authors:
152
+ element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
153
+ element.attrib[f'{{{NS["rdf"]}}}about'] = ''
154
+ element = SubElement(element, f'{{{NS["dc"]}}}creator')
155
+ element = SubElement(element, f'{{{NS["rdf"]}}}Seq')
156
+ for author in self.authors:
157
+ author_element = SubElement(element, f'{{{NS["rdf"]}}}li')
158
+ author_element.text = author
159
+ if self.description:
160
+ element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
161
+ element.attrib[f'{{{NS["rdf"]}}}about'] = ''
162
+ element = SubElement(element, f'{{{NS["dc"]}}}subject')
163
+ element = SubElement(element, f'{{{NS["rdf"]}}}Bag')
164
+ element = SubElement(element, f'{{{NS["rdf"]}}}li')
165
+ element.attrib['xml:lang'] = 'x-default'
166
+ element.text = self.description
167
+ if self.keywords:
168
+ element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
169
+ element.attrib[f'{{{NS["rdf"]}}}about'] = ''
170
+ element = SubElement(element, f'{{{NS["pdf"]}}}Keywords')
171
+ element.text = ', '.join(self.keywords)
172
+ if self.generator:
173
+ element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
174
+ element.attrib[f'{{{NS["rdf"]}}}about'] = ''
175
+ element = SubElement(element, f'{{{NS["xmp"]}}}CreatorTool')
176
+ element.text = self.generator
177
+ if self.created:
178
+ element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
179
+ element.attrib[f'{{{NS["rdf"]}}}about'] = ''
180
+ element = SubElement(element, f'{{{NS["xmp"]}}}CreateDate')
181
+ element.text = self.created
182
+ if self.modified:
183
+ element = SubElement(rdf, f'{{{NS["rdf"]}}}Description')
184
+ element.attrib[f'{{{NS["rdf"]}}}about'] = ''
185
+ element = SubElement(element, f'{{{NS["xmp"]}}}ModifyDate')
186
+ element.text = self.modified
187
+ return tostring(rdf, encoding='utf-8')
weasyprint/pdf/pdfa.py CHANGED
@@ -4,8 +4,6 @@ from functools import partial
4
4
 
5
5
  import pydyf
6
6
 
7
- from .metadata import add_metadata
8
-
9
7
 
10
8
  def pdfa(pdf, metadata, document, page_streams, attachments, compress,
11
9
  version, variant):
@@ -65,7 +63,7 @@ def pdfa(pdf, metadata, document, page_streams, attachments, compress,
65
63
  if version == 1:
66
64
  # Metadata compression is forbidden for version 1.
67
65
  compress = False
68
- add_metadata(pdf, metadata, 'a', version, variant, compress)
66
+ metadata.include_in_pdf(pdf, 'a', version, variant, compress)
69
67
 
70
68
  # Remove document information.
71
69
  if version >= 4:
weasyprint/pdf/pdfua.py CHANGED
@@ -2,13 +2,11 @@
2
2
 
3
3
  from functools import partial
4
4
 
5
- from .metadata import add_metadata
6
-
7
5
 
8
6
  def pdfua(pdf, metadata, document, page_streams, attachments, compress, version):
9
7
  """Set metadata for PDF/UA documents."""
10
8
  # Common PDF metadata stream
11
- add_metadata(pdf, metadata, 'ua', version, conformance=None, compress=compress)
9
+ metadata.include_in_pdf(pdf, 'ua', version, conformance=None, compress=compress)
12
10
 
13
11
 
14
12
  VARIANTS = {
weasyprint/pdf/pdfx.py CHANGED
@@ -5,8 +5,6 @@ from time import localtime
5
5
 
6
6
  import pydyf
7
7
 
8
- from .metadata import add_metadata
9
-
10
8
 
11
9
  def pdfx(pdf, metadata, document, page_streams, attachments, compress, version,
12
10
  variant):
@@ -47,7 +45,7 @@ def pdfx(pdf, metadata, document, page_streams, attachments, compress, version,
47
45
  ])
48
46
 
49
47
  # Common PDF metadata stream.
50
- add_metadata(pdf, metadata, 'x', version, conformance, compress=compress)
48
+ metadata.include_in_pdf(pdf, 'x', version, conformance, compress=compress)
51
49
 
52
50
 
53
51
  VARIANTS = {
@@ -9,7 +9,7 @@ from cssselect2 import ElementWrapper
9
9
 
10
10
  from ..urls import get_url_attribute
11
11
  from .css import parse_declarations, parse_stylesheets
12
- from .defs import apply_filters, clip_path, draw_gradient_or_pattern, paint_mask, use
12
+ from .defs import apply_filters, draw_gradient_or_pattern, paint_mask, use
13
13
  from .images import image, svg
14
14
  from .path import path
15
15
  from .shapes import circle, ellipse, line, polygon, polyline, rect
@@ -24,7 +24,6 @@ from .utils import ( # isort:skip
24
24
  TAGS = {
25
25
  'a': text,
26
26
  'circle': circle,
27
- 'clipPath': clip_path,
28
27
  'ellipse': ellipse,
29
28
  'image': image,
30
29
  'line': line,
@@ -155,6 +154,14 @@ class Node:
155
154
  for name, value in declarations:
156
155
  child.attrib[name] = value.strip()
157
156
 
157
+ # Expand
158
+ # TODO: simplified expanders, use CSS expander code instead.
159
+ if font := child.attrib.pop('font', None):
160
+ parts = font.strip().split(maxsplit=1)
161
+ if len(parts) == 2:
162
+ child.attrib['font-size'] = parts[0]
163
+ child.attrib['font-family'] = parts[1]
164
+
158
165
  # Replace 'currentColor' value
159
166
  for key in COLOR_ATTRIBUTES:
160
167
  if child.get(key) == 'currentColor':
@@ -219,6 +226,8 @@ class Node:
219
226
 
220
227
  def get_child(self, id_):
221
228
  """Get a child with given id in the whole child tree."""
229
+ if self._etree_node.find(f'.//*[@id="{id_}"]') is None:
230
+ return
222
231
  for child in self:
223
232
  if child.get('id') == id_:
224
233
  return child
@@ -324,23 +333,52 @@ class Node:
324
333
  svg.inner_diagonal = hypot(svg.inner_width, svg.inner_height) / sqrt(2)
325
334
 
326
335
 
336
+ class LazyDefs:
337
+ def __init__(self, name, svg):
338
+ self._name = name
339
+ self._svg = svg
340
+ self._data = {}
341
+
342
+ def __getitem__(self, name):
343
+ return self.get(name)
344
+
345
+ def get(self, name):
346
+ if not name:
347
+ return
348
+ if name in self._data:
349
+ return self._data[name]
350
+ node = self._svg.tree.get_child(name)
351
+ if node is not None and self._name in node.tag.lower():
352
+ self._data[name] = node
353
+ if self._name in ('gradient', 'pattern'):
354
+ self._svg.inherit_element(node, self)
355
+ else:
356
+ self._data[name] = None
357
+ return self._data[name]
358
+
359
+ def __contains__(self, name):
360
+ return self.get(name)
361
+
362
+
327
363
  class SVG:
328
364
  """An SVG document."""
329
365
 
330
- def __init__(self, tree, url, font_config):
366
+ def __init__(self, tree, url, font_config, url_fetcher=None):
331
367
  wrapper = ElementWrapper.from_xml_root(tree)
332
- style = parse_stylesheets(wrapper, url)
368
+ style = parse_stylesheets(wrapper, url, font_config, url_fetcher)
333
369
  self.tree = Node(wrapper, style)
334
370
  self.font_config = font_config
371
+ self.url_fetcher = url_fetcher
335
372
  self.url = url
336
- self.filters = {}
337
- self.gradients = {}
338
- self.images = {}
339
- self.markers = {}
340
- self.masks = {}
341
- self.patterns = {}
342
- self.paths = {}
343
- self.symbols = {}
373
+
374
+ self.filters = LazyDefs('filter', self)
375
+ self.gradients = LazyDefs('gradient', self)
376
+ self.images = LazyDefs('image', self)
377
+ self.markers = LazyDefs('marker', self)
378
+ self.masks = LazyDefs('mask', self)
379
+ self.patterns = LazyDefs('pattern', self)
380
+ self.paths = LazyDefs('path', self)
381
+ self.symbols = LazyDefs('symbol', self)
344
382
 
345
383
  self.use_cache = {}
346
384
 
@@ -349,8 +387,6 @@ class SVG:
349
387
  self.text_path_width = 0
350
388
 
351
389
  self.tree.cascade(self.tree)
352
- self.parse_defs(self.tree)
353
- self.inherit_defs()
354
390
 
355
391
  def get_intrinsic_size(self, font_size):
356
392
  """Get intrinsic size of the image."""
@@ -382,15 +418,13 @@ class SVG:
382
418
  """Compute size of an arbirtary attribute."""
383
419
  return size(length, font_size, self.inner_diagonal)
384
420
 
385
- def draw(self, stream, concrete_width, concrete_height, base_url,
386
- url_fetcher, context):
421
+ def draw(self, stream, concrete_width, concrete_height, base_url, context):
387
422
  """Draw image on a stream."""
388
423
  self.stream = stream
389
424
 
390
425
  self.tree.set_svg_size(self, concrete_width, concrete_height)
391
426
 
392
427
  self.base_url = base_url
393
- self.url_fetcher = url_fetcher
394
428
  self.context = context
395
429
 
396
430
  self.draw_node(self.tree, size('12pt'))
@@ -796,20 +830,6 @@ class SVG:
796
830
  if matrix.determinant:
797
831
  self.stream.transform(*matrix.values)
798
832
 
799
- def parse_defs(self, node):
800
- """Parse defs included in a tree."""
801
- for def_type in DEF_TYPES:
802
- if def_type in node.tag.lower() and 'id' in node.attrib:
803
- getattr(self, f'{def_type}s')[node.attrib['id']] = node
804
- for child in node:
805
- self.parse_defs(child)
806
-
807
- def inherit_defs(self):
808
- """Handle inheritance of different defined elements lists."""
809
- for defs in (self.gradients, self.patterns):
810
- for element in defs.values():
811
- self.inherit_element(element, defs)
812
-
813
833
  def inherit_element(self, element, defs):
814
834
  """Recursively handle inheritance of defined element."""
815
835
  href = element.get_href(self.url)
@@ -840,7 +860,7 @@ class SVG:
840
860
  class Pattern(SVG):
841
861
  """SVG node applied as a pattern."""
842
862
  def __init__(self, tree, svg):
843
- super().__init__(tree._etree_node, svg.url, svg.font_config)
863
+ super().__init__(tree._etree_node, svg.url, svg.font_config, svg.url_fetcher)
844
864
  self.svg = svg
845
865
  self.tree = tree
846
866
 
weasyprint/svg/css.py CHANGED
@@ -5,11 +5,12 @@ from urllib.parse import urljoin
5
5
  import cssselect2
6
6
  import tinycss2
7
7
 
8
+ from ..css.validation.descriptors import preprocess_descriptors
8
9
  from ..logger import LOGGER
9
10
  from .utils import parse_url
10
11
 
11
12
 
12
- def find_stylesheets_rules(tree, stylesheet_rules, url):
13
+ def find_stylesheets_rules(tree, stylesheet_rules, url, font_config, url_fetcher):
13
14
  """Find rules among stylesheet rules and imports."""
14
15
  for rule in stylesheet_rules:
15
16
  if rule.type == 'at-rule':
@@ -22,7 +23,22 @@ def find_stylesheets_rules(tree, stylesheet_rules, url):
22
23
  stylesheet = tinycss2.parse_stylesheet(
23
24
  tree.fetch_url(css_url, 'text/css').decode())
24
25
  url = css_url.geturl()
25
- yield from find_stylesheets_rules(tree, stylesheet, url)
26
+ yield from find_stylesheets_rules(
27
+ tree, stylesheet, url, font_config, url_fetcher)
28
+ elif rule.lower_at_keyword == 'font-face':
29
+ if font_config is not None and url_fetcher is not None:
30
+ content = tinycss2.parse_blocks_contents(rule.content)
31
+ rule_descriptors = dict(
32
+ preprocess_descriptors('font-face', url, content))
33
+ for key in ('src', 'font_family'):
34
+ if key not in rule_descriptors:
35
+ LOGGER.warning(
36
+ "Missing %s descriptor in '@font-face' rule at "
37
+ "%d:%d", key.replace('_', '-'),
38
+ rule.source_line, rule.source_column)
39
+ break
40
+ else:
41
+ font_config.add_font_face(rule_descriptors, url_fetcher)
26
42
  # TODO: support media types
27
43
  # if rule.lower_at_keyword == 'media':
28
44
  elif rule.type == 'qualified-rule':
@@ -49,7 +65,7 @@ def parse_declarations(input):
49
65
  return normal_declarations, important_declarations
50
66
 
51
67
 
52
- def parse_stylesheets(tree, url):
68
+ def parse_stylesheets(tree, url, font_config, url_fetcher):
53
69
  """Find stylesheets and return rule matchers in given tree."""
54
70
  normal_matcher = cssselect2.Matcher()
55
71
  important_matcher = cssselect2.Matcher()
@@ -70,7 +86,8 @@ def parse_stylesheets(tree, url):
70
86
 
71
87
  # Parse rules and fill matchers
72
88
  for stylesheet in stylesheets:
73
- for rule in find_stylesheets_rules(tree, stylesheet, url):
89
+ for rule in find_stylesheets_rules(
90
+ tree, stylesheet, url, font_config, url_fetcher):
74
91
  normal_declarations, important_declarations = parse_declarations(
75
92
  rule.content)
76
93
  try:
weasyprint/svg/defs.py CHANGED
@@ -102,10 +102,12 @@ def draw_gradient(svg, node, gradient, font_size, opacity, stroke):
102
102
  return False
103
103
  if gradient.get('gradientUnits') == 'userSpaceOnUse':
104
104
  width, height = svg.inner_width, svg.inner_height
105
+ bx1, by1 = bounding_box[:2]
105
106
  matrix = Matrix()
106
107
  else:
107
108
  width, height = 1, 1
108
109
  e, f, a, d = bounding_box
110
+ bx1, by1 = 0, 0
109
111
  matrix = Matrix(a=a, d=d, e=e, f=f)
110
112
 
111
113
  spread = gradient.get('spreadMethod', 'pad')
@@ -180,10 +182,10 @@ def draw_gradient(svg, node, gradient, font_size, opacity, stroke):
180
182
  if 0 not in (a0, a1) and (a0, a1) != (1, 1):
181
183
  color_couples[i][2] = a0 / a1
182
184
 
183
- bx1, by1 = 0, 0
184
185
  if 'gradientTransform' in gradient.attrib:
186
+ bx2, by2 = bx1 + width, by1 + height
185
187
  bx1, by1 = transform_matrix.invert.transform_point(bx1, by1)
186
- bx2, by2 = transform_matrix.invert.transform_point(width, height)
188
+ bx2, by2 = transform_matrix.invert.transform_point(bx2, by2)
187
189
  width, height = bx2 - bx1, by2 - by1
188
190
 
189
191
  # Ensure that width and height are positive to please some PDF readers
@@ -457,7 +459,7 @@ def draw_pattern(svg, node, pattern, font_size, opacity, stroke):
457
459
  group = stream_pattern.add_group(0, 0, pattern_width, pattern_height)
458
460
  Pattern(pattern, svg).draw(
459
461
  group, pattern_width, pattern_height, svg.base_url,
460
- svg.url_fetcher, svg.context)
462
+ svg.context)
461
463
  stream_pattern.draw_x_object(group.id)
462
464
  svg.stream.set_color_space('Pattern', stroke=stroke)
463
465
  svg.stream.set_color_special(stream_pattern.id, stroke=stroke)
@@ -515,9 +517,3 @@ def paint_mask(svg, node, mask, font_size):
515
517
  svg.stream = svg.stream.set_alpha_state(x, y, width, height)
516
518
  svg.draw_node(mask, font_size)
517
519
  svg.stream = svg_stream
518
-
519
-
520
- def clip_path(svg, node, font_size):
521
- """Store a clip path definition."""
522
- if 'id' in node.attrib:
523
- svg.paths[node.attrib['id']] = node
weasyprint/svg/text.py CHANGED
@@ -141,7 +141,7 @@ def text(svg, node, font_size):
141
141
  svg.cursor_d_position[1] = 0
142
142
  svg.cursor_d_position[0] += dx or 0
143
143
  svg.cursor_d_position[1] += dy or 0
144
- layout, _, _, width, height, _ = split_first_line(
144
+ layout, _, _, width, height, baseline = split_first_line(
145
145
  letter, style, svg.context, inf, 0)
146
146
  x = svg.cursor_position[0] if x is None else x
147
147
  y = svg.cursor_position[1] if y is None else y
@@ -154,8 +154,9 @@ def text(svg, node, font_size):
154
154
  y_position = y + svg.cursor_d_position[1] + y_align
155
155
  angle = last_r if r is None else r
156
156
  points = (
157
- (x_position, y_position),
158
- (x_position + width, y_position - height))
157
+ (x_position, y_position - baseline),
158
+ (x_position + width, y_position - baseline + height))
159
+ # TODO: Use ink extents instead of logical from line_break.line_size().
159
160
  node.text_bounding_box = extend_bounding_box(
160
161
  node.text_bounding_box, points)
161
162
 
weasyprint/text/fonts.py CHANGED
@@ -167,9 +167,8 @@ class FontConfiguration:
167
167
 
168
168
  # Get font content.
169
169
  try:
170
- with fetch(url_fetcher, url) as result:
171
- string = 'string' in result
172
- font = result['string'] if string else result['file_obj'].read()
170
+ with fetch(url_fetcher, url) as response:
171
+ font = response.read()
173
172
  except Exception as exception:
174
173
  LOGGER.debug('Failed to load font at %r (%s)', url, exception)
175
174
  continue
@@ -256,6 +256,8 @@ def split_first_line(text, style, context, max_width, justification_spacing,
256
256
  ``baseline``: baseline in pixels of the first line
257
257
 
258
258
  """
259
+ from ..layout.percent import percentage
260
+
259
261
  # See https://www.w3.org/TR/css-text-3/#white-space-property
260
262
  text_wrap = style['white_space'] in ('normal', 'pre-wrap', 'pre-line')
261
263
  space_collapse = style['white_space'] in ('normal', 'nowrap', 'pre-line')
@@ -392,11 +394,8 @@ def split_first_line(text, style, context, max_width, justification_spacing,
392
394
  # This word is long enough.
393
395
  first_line_width, _ = line_size(first_line, style)
394
396
  space = max_width - first_line_width
395
- if style['hyphenate_limit_zone'].unit == '%':
396
- limit_zone = (
397
- max_width * style['hyphenate_limit_zone'].value / 100)
398
- else:
399
- limit_zone = style['hyphenate_limit_zone'].value
397
+ limit_zone = percentage(
398
+ style['hyphenate_limit_zone'], style, max_width)
400
399
  if space > limit_zone or space < 0:
401
400
  # Available space is worth the try, or the line is even too long
402
401
  # to fit: try to hyphenate.