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.
- weasyprint/__init__.py +35 -103
- weasyprint/__main__.py +107 -80
- weasyprint/css/__init__.py +7 -14
- weasyprint/css/functions.py +5 -0
- weasyprint/css/html5_ua.css +1 -1
- weasyprint/css/tokens.py +4 -1
- weasyprint/css/validation/properties.py +4 -4
- weasyprint/document.py +4 -64
- weasyprint/draw/text.py +12 -10
- weasyprint/formatting_structure/boxes.py +4 -1
- weasyprint/formatting_structure/build.py +111 -37
- weasyprint/images.py +27 -32
- weasyprint/layout/__init__.py +2 -1
- weasyprint/layout/block.py +22 -16
- weasyprint/layout/grid.py +25 -14
- weasyprint/layout/page.py +4 -4
- weasyprint/layout/preferred.py +63 -24
- weasyprint/pdf/__init__.py +12 -1
- weasyprint/pdf/anchors.py +10 -16
- weasyprint/pdf/fonts.py +12 -3
- weasyprint/pdf/metadata.py +153 -98
- weasyprint/pdf/pdfa.py +1 -3
- weasyprint/pdf/pdfua.py +1 -3
- weasyprint/pdf/pdfx.py +1 -3
- weasyprint/svg/__init__.py +52 -32
- weasyprint/svg/css.py +21 -4
- weasyprint/svg/defs.py +5 -9
- weasyprint/svg/text.py +4 -3
- weasyprint/text/fonts.py +2 -3
- weasyprint/text/line_break.py +4 -5
- weasyprint/urls.py +290 -97
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/METADATA +2 -1
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/RECORD +36 -36
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/WHEEL +0 -0
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/entry_points.txt +0 -0
- {weasyprint-67.0.dist-info → weasyprint-68.1.dist-info}/licenses/LICENSE +0 -0
weasyprint/pdf/metadata.py
CHANGED
|
@@ -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
|
-
|
|
27
|
-
"""
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
43
|
-
|
|
84
|
+
def include_in_pdf(self, pdf, variant, version, conformance, compress):
|
|
85
|
+
"""Add PDF stream of metadata.
|
|
44
86
|
|
|
45
|
-
|
|
87
|
+
Described in ISO-32000-1:2008, 14.3.2.
|
|
46
88
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
metadata.include_in_pdf(pdf, 'x', version, conformance, compress=compress)
|
|
51
49
|
|
|
52
50
|
|
|
53
51
|
VARIANTS = {
|
weasyprint/svg/__init__.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
337
|
-
self.
|
|
338
|
-
self.
|
|
339
|
-
self.
|
|
340
|
-
self.
|
|
341
|
-
self.
|
|
342
|
-
self.
|
|
343
|
-
self.
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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,
|
|
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
|
|
171
|
-
|
|
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
|
weasyprint/text/line_break.py
CHANGED
|
@@ -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
|
-
|
|
396
|
-
|
|
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.
|