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/__init__.py
CHANGED
|
@@ -5,7 +5,6 @@ importing sub-modules.
|
|
|
5
5
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import contextlib
|
|
9
8
|
from datetime import datetime
|
|
10
9
|
from os.path import getctime, getmtime
|
|
11
10
|
from pathlib import Path
|
|
@@ -15,9 +14,9 @@ import cssselect2
|
|
|
15
14
|
import tinycss2
|
|
16
15
|
import tinyhtml5
|
|
17
16
|
|
|
18
|
-
VERSION = __version__ = '
|
|
17
|
+
VERSION = __version__ = '68.1'
|
|
19
18
|
|
|
20
|
-
#: Default values for command-line and Python API options. See
|
|
19
|
+
#: Default values for command-line and Python API rendering options. See
|
|
21
20
|
#: :func:`__main__.main` to learn more about specific options for
|
|
22
21
|
#: command-line.
|
|
23
22
|
#:
|
|
@@ -67,14 +66,15 @@ VERSION = __version__ = '67.0'
|
|
|
67
66
|
#: images are temporarily stored.
|
|
68
67
|
DEFAULT_OPTIONS = {
|
|
69
68
|
'stylesheets': None,
|
|
70
|
-
'media_type': 'print',
|
|
71
69
|
'attachments': None,
|
|
70
|
+
'attachment_relationships': None,
|
|
72
71
|
'pdf_identifier': None,
|
|
73
72
|
'pdf_variant': None,
|
|
74
73
|
'pdf_version': None,
|
|
75
74
|
'pdf_forms': None,
|
|
76
75
|
'pdf_tags': False,
|
|
77
76
|
'uncompressed_pdf': False,
|
|
77
|
+
'xmp_metadata': None,
|
|
78
78
|
'custom_metadata': False,
|
|
79
79
|
'presentational_hints': False,
|
|
80
80
|
'srgb': False,
|
|
@@ -92,8 +92,7 @@ __all__ = [
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
# Import after setting the version, as the version is used in other modules
|
|
95
|
-
from .urls import
|
|
96
|
-
fetch, default_url_fetcher, path2url, ensure_url, url_is_absolute)
|
|
95
|
+
from .urls import URLFetcher, default_url_fetcher, select_source # noqa: I001, E402
|
|
97
96
|
from .logger import LOGGER, PROGRESS_LOGGER # noqa: E402
|
|
98
97
|
# Some imports are at the end of the file (after the CSS class)
|
|
99
98
|
# to work around circular imports.
|
|
@@ -150,9 +149,7 @@ class HTML:
|
|
|
150
149
|
:term:`file objects <file object>`.
|
|
151
150
|
:type url_fetcher: :term:`callable`
|
|
152
151
|
:param url_fetcher:
|
|
153
|
-
|
|
154
|
-
:func:`default_url_fetcher` called to fetch external resources such as
|
|
155
|
-
stylesheets and images. (See :ref:`URL Fetchers`.)
|
|
152
|
+
An instance of :class:`urls.URLFetcher`. (See :ref:`URL Fetchers`.)
|
|
156
153
|
:param str media_type:
|
|
157
154
|
The media type to use for ``@media``. Defaults to ``'print'``.
|
|
158
155
|
**Note:** In some cases like ``HTML(string=foo)`` relative URLs will be
|
|
@@ -161,25 +158,24 @@ class HTML:
|
|
|
161
158
|
"""
|
|
162
159
|
def __init__(self, guess=None, filename=None, url=None, file_obj=None,
|
|
163
160
|
string=None, encoding=None, base_url=None,
|
|
164
|
-
url_fetcher=
|
|
161
|
+
url_fetcher=None, media_type='print'):
|
|
165
162
|
PROGRESS_LOGGER.info(
|
|
166
163
|
'Step 1 - Fetching and parsing HTML - %s',
|
|
167
164
|
guess or filename or url or
|
|
168
165
|
getattr(file_obj, 'name', 'HTML string'))
|
|
169
166
|
if isinstance(base_url, Path):
|
|
170
167
|
base_url = str(base_url)
|
|
171
|
-
|
|
168
|
+
if url_fetcher is None:
|
|
169
|
+
url_fetcher = URLFetcher()
|
|
170
|
+
result = select_source(
|
|
172
171
|
guess, filename, url, file_obj, string, base_url, url_fetcher)
|
|
173
|
-
with result as (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if encoding is not None:
|
|
181
|
-
kwargs['override_encoding'] = encoding
|
|
182
|
-
result = tinyhtml5.parse(source, **kwargs)
|
|
172
|
+
with result as (file_obj, base_url, protocol_encoding, _):
|
|
173
|
+
kwargs = {'namespace_html_elements': False}
|
|
174
|
+
if protocol_encoding is not None:
|
|
175
|
+
kwargs['transport_encoding'] = protocol_encoding
|
|
176
|
+
if encoding is not None:
|
|
177
|
+
kwargs['override_encoding'] = encoding
|
|
178
|
+
result = tinyhtml5.parse(file_obj, **kwargs)
|
|
183
179
|
self.base_url = _find_base_url(result, base_url)
|
|
184
180
|
self.url_fetcher = url_fetcher
|
|
185
181
|
self.media_type = media_type
|
|
@@ -286,28 +282,26 @@ class CSS:
|
|
|
286
282
|
of :class:`HTML` objects.
|
|
287
283
|
|
|
288
284
|
"""
|
|
289
|
-
def __init__(self, guess=None, filename=None, url=None, file_obj=None,
|
|
290
|
-
|
|
291
|
-
url_fetcher=default_url_fetcher, _check_mime_type=False,
|
|
285
|
+
def __init__(self, guess=None, filename=None, url=None, file_obj=None, string=None,
|
|
286
|
+
encoding=None, base_url=None, url_fetcher=None, _check_mime_type=False,
|
|
292
287
|
media_type='print', font_config=None, counter_style=None,
|
|
293
288
|
color_profiles=None, matcher=None, page_rules=None, layers=None,
|
|
294
289
|
layer=None):
|
|
295
290
|
PROGRESS_LOGGER.info(
|
|
296
291
|
'Step 2 - Fetching and parsing CSS - %s',
|
|
297
292
|
filename or url or getattr(file_obj, 'name', 'CSS string'))
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if isinstance(
|
|
306
|
-
|
|
307
|
-
stylesheet = tinycss2.parse_stylesheet(source)
|
|
293
|
+
if url_fetcher is None:
|
|
294
|
+
url_fetcher = URLFetcher()
|
|
295
|
+
result = select_source(
|
|
296
|
+
guess, filename, url, file_obj, string, base_url=base_url,
|
|
297
|
+
url_fetcher=url_fetcher, check_css_mime_type=_check_mime_type)
|
|
298
|
+
with result as (file_obj, base_url, protocol_encoding, mime_type):
|
|
299
|
+
css = file_obj.read()
|
|
300
|
+
if isinstance(css, str):
|
|
301
|
+
stylesheet = tinycss2.parse_stylesheet(css)
|
|
308
302
|
else:
|
|
309
|
-
stylesheet,
|
|
310
|
-
|
|
303
|
+
stylesheet, _ = tinycss2.parse_stylesheet_bytes(
|
|
304
|
+
css, environment_encoding=encoding,
|
|
311
305
|
protocol_encoding=protocol_encoding)
|
|
312
306
|
self.base_url = base_url
|
|
313
307
|
self.matcher = matcher or cssselect2.Matcher()
|
|
@@ -347,10 +341,12 @@ class Attachment:
|
|
|
347
341
|
|
|
348
342
|
"""
|
|
349
343
|
def __init__(self, guess=None, filename=None, url=None, file_obj=None,
|
|
350
|
-
string=None, base_url=None, url_fetcher=
|
|
351
|
-
|
|
344
|
+
string=None, base_url=None, url_fetcher=None, name=None,
|
|
345
|
+
description=None, created=None, modified=None,
|
|
352
346
|
relationship='Unspecified'):
|
|
353
|
-
|
|
347
|
+
if url_fetcher is None:
|
|
348
|
+
url_fetcher = URLFetcher()
|
|
349
|
+
self.source = select_source(
|
|
354
350
|
guess, filename, url, file_obj, string, base_url=base_url,
|
|
355
351
|
url_fetcher=url_fetcher)
|
|
356
352
|
self.name = name
|
|
@@ -372,70 +368,6 @@ class Attachment:
|
|
|
372
368
|
self.modified = modified
|
|
373
369
|
|
|
374
370
|
|
|
375
|
-
@contextlib.contextmanager
|
|
376
|
-
def _select_source(guess=None, filename=None, url=None, file_obj=None,
|
|
377
|
-
string=None, base_url=None, url_fetcher=default_url_fetcher,
|
|
378
|
-
check_css_mime_type=False):
|
|
379
|
-
"""If only one input is given, return it with normalized ``base_url``."""
|
|
380
|
-
if base_url is not None:
|
|
381
|
-
base_url = ensure_url(base_url)
|
|
382
|
-
|
|
383
|
-
selected_params = [
|
|
384
|
-
param for param in (guess, filename, url, file_obj, string) if
|
|
385
|
-
param is not None]
|
|
386
|
-
if len(selected_params) != 1:
|
|
387
|
-
source = ', '.join(selected_params) or 'nothing'
|
|
388
|
-
raise TypeError(f'Expected exactly one source, got {source}')
|
|
389
|
-
elif guess is not None:
|
|
390
|
-
if hasattr(guess, 'read'):
|
|
391
|
-
type_ = 'file_obj'
|
|
392
|
-
elif isinstance(guess, Path):
|
|
393
|
-
type_ = 'filename'
|
|
394
|
-
elif url_is_absolute(guess):
|
|
395
|
-
type_ = 'url'
|
|
396
|
-
else:
|
|
397
|
-
type_ = 'filename'
|
|
398
|
-
result = _select_source(
|
|
399
|
-
base_url=base_url, url_fetcher=url_fetcher,
|
|
400
|
-
check_css_mime_type=check_css_mime_type,
|
|
401
|
-
**{type_: guess})
|
|
402
|
-
with result as result:
|
|
403
|
-
yield result
|
|
404
|
-
elif filename is not None:
|
|
405
|
-
if base_url is None:
|
|
406
|
-
base_url = path2url(filename)
|
|
407
|
-
with open(filename, 'rb') as file_obj:
|
|
408
|
-
yield 'file_obj', file_obj, base_url, None
|
|
409
|
-
elif url is not None:
|
|
410
|
-
with fetch(url_fetcher, url) as result:
|
|
411
|
-
if check_css_mime_type and result['mime_type'] != 'text/css':
|
|
412
|
-
LOGGER.error(
|
|
413
|
-
'Unsupported stylesheet type %s for %s',
|
|
414
|
-
result['mime_type'], result['redirected_url'])
|
|
415
|
-
yield 'string', '', base_url, None
|
|
416
|
-
else:
|
|
417
|
-
proto_encoding = result.get('encoding')
|
|
418
|
-
if base_url is None:
|
|
419
|
-
base_url = result.get('redirected_url', url)
|
|
420
|
-
if 'string' in result:
|
|
421
|
-
yield 'string', result['string'], base_url, proto_encoding
|
|
422
|
-
else:
|
|
423
|
-
yield (
|
|
424
|
-
'file_obj', result['file_obj'], base_url,
|
|
425
|
-
proto_encoding)
|
|
426
|
-
elif file_obj is not None:
|
|
427
|
-
if base_url is None:
|
|
428
|
-
# filesystem file-like objects have a 'name' attribute.
|
|
429
|
-
name = getattr(file_obj, 'name', None)
|
|
430
|
-
# Some streams have a .name like '<stdin>', not a filename.
|
|
431
|
-
if name and not name.startswith('<'):
|
|
432
|
-
base_url = ensure_url(name)
|
|
433
|
-
yield 'file_obj', file_obj, base_url, None
|
|
434
|
-
else:
|
|
435
|
-
assert string is not None
|
|
436
|
-
yield 'string', string, base_url, None
|
|
437
|
-
|
|
438
|
-
|
|
439
371
|
# Work around circular imports.
|
|
440
372
|
from .css import preprocess_stylesheet # noqa: I001, E402
|
|
441
373
|
from .html import ( # noqa: E402
|
weasyprint/__main__.py
CHANGED
|
@@ -4,14 +4,13 @@ import argparse
|
|
|
4
4
|
import logging
|
|
5
5
|
import platform
|
|
6
6
|
import sys
|
|
7
|
-
from functools import partial
|
|
8
7
|
|
|
9
8
|
import pydyf
|
|
10
9
|
|
|
11
10
|
from . import DEFAULT_OPTIONS, HTML, LOGGER, __version__
|
|
12
11
|
from .pdf import VARIANTS
|
|
13
12
|
from .text.ffi import pango
|
|
14
|
-
from .urls import
|
|
13
|
+
from .urls import URLFetcher
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class PrintInfo(argparse.Action):
|
|
@@ -33,117 +32,141 @@ class PrintInfo(argparse.Action):
|
|
|
33
32
|
|
|
34
33
|
class Parser(argparse.ArgumentParser):
|
|
35
34
|
def __init__(self, *args, **kwargs):
|
|
36
|
-
self.
|
|
35
|
+
self._groups = {None: {}}
|
|
37
36
|
super().__init__(*args, **kwargs)
|
|
38
37
|
|
|
39
|
-
def add_argument(self, *args, **kwargs):
|
|
40
|
-
|
|
38
|
+
def add_argument(self, *args, _group_name=None, **kwargs):
|
|
39
|
+
if _group_name is None:
|
|
40
|
+
super().add_argument(*args, **kwargs)
|
|
41
41
|
key = args[-1].lstrip('-')
|
|
42
42
|
kwargs['flags'] = args
|
|
43
43
|
kwargs['positional'] = args[-1][0] != '-'
|
|
44
|
-
self.
|
|
44
|
+
self._groups[_group_name][key] = kwargs
|
|
45
|
+
|
|
46
|
+
def add_argument_group(self, name, *args, **kwargs):
|
|
47
|
+
group = super().add_argument_group(name, *args, **kwargs)
|
|
48
|
+
self._groups[name] = {}
|
|
49
|
+
def add_argument(*args, **kwargs):
|
|
50
|
+
group._add_argument(*args, **kwargs)
|
|
51
|
+
self.add_argument(*args, _group_name=name, **kwargs)
|
|
52
|
+
group._add_argument = group.add_argument
|
|
53
|
+
group.add_argument = add_argument
|
|
54
|
+
return group
|
|
45
55
|
|
|
46
56
|
@property
|
|
47
57
|
def docstring(self):
|
|
48
|
-
self.
|
|
58
|
+
self._groups[None].pop('help')
|
|
49
59
|
data = []
|
|
50
|
-
for
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
data.append(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
data.append('
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
data
|
|
60
|
+
for group, arguments in self._groups.items():
|
|
61
|
+
if not arguments:
|
|
62
|
+
continue
|
|
63
|
+
if group:
|
|
64
|
+
data.append(f'{group[0].title()}{group[1:]}\n')
|
|
65
|
+
data.append(f'{"~" * len(group)}\n\n')
|
|
66
|
+
for key, args in arguments.items():
|
|
67
|
+
data.append('.. option:: ')
|
|
68
|
+
action = args.get('action', 'store')
|
|
69
|
+
for flag in args['flags']:
|
|
70
|
+
data.append(flag)
|
|
71
|
+
if not args['positional'] and action in ('store', 'append'):
|
|
72
|
+
data.append(f' <{key}>')
|
|
73
|
+
data.append(', ')
|
|
74
|
+
data[-1] = '\n\n'
|
|
75
|
+
data.append(f' {args["help"][0].upper()}{args["help"][1:]}.\n\n')
|
|
76
|
+
if 'choices' in args:
|
|
77
|
+
choices = ", ".join(args['choices'])
|
|
78
|
+
data.append(f' Possible choices: {choices}.\n\n')
|
|
79
|
+
if action == 'append':
|
|
80
|
+
data.append(' This option can be passed multiple times.\n\n')
|
|
65
81
|
return ''.join(data)
|
|
66
82
|
|
|
67
83
|
|
|
68
84
|
PARSER = Parser(prog='weasyprint', description='Render web pages to PDF.')
|
|
85
|
+
PARSER.add_argument('input', help='URL or filename of the HTML input, or - for stdin')
|
|
86
|
+
PARSER.add_argument('output', help='filename where output is written, or - for stdout')
|
|
69
87
|
PARSER.add_argument(
|
|
70
|
-
'
|
|
71
|
-
PARSER.add_argument(
|
|
72
|
-
'output', help='filename where output is written, or - for stdout')
|
|
73
|
-
PARSER.add_argument(
|
|
74
|
-
'-e', '--encoding', help='force the input character encoding')
|
|
88
|
+
'-i', '--info', action=PrintInfo, nargs=0, help='print system information and exit')
|
|
75
89
|
PARSER.add_argument(
|
|
90
|
+
'--version', action='version', version=f'WeasyPrint version {__version__}',
|
|
91
|
+
help='print WeasyPrint’s version number and exit')
|
|
92
|
+
|
|
93
|
+
group = PARSER.add_argument_group('rendering options')
|
|
94
|
+
group.add_argument(
|
|
76
95
|
'-s', '--stylesheet', action='append', dest='stylesheets',
|
|
77
96
|
help='URL or filename for a user CSS stylesheet')
|
|
78
|
-
|
|
79
|
-
'-m', '--media-type',
|
|
80
|
-
help='media type to use for @media, defaults to print')
|
|
81
|
-
PARSER.add_argument(
|
|
82
|
-
'-u', '--base-url',
|
|
83
|
-
help='base for relative URLs in the HTML input, defaults to the '
|
|
84
|
-
'input’s own filename or URL or the current directory for stdin')
|
|
85
|
-
PARSER.add_argument(
|
|
97
|
+
group.add_argument(
|
|
86
98
|
'-a', '--attachment', action='append', dest='attachments',
|
|
87
99
|
help='URL or filename of a file to attach to the PDF document')
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
group.add_argument(
|
|
101
|
+
'--attachment-relationship', action='append', dest='attachment_relationships',
|
|
102
|
+
help='Relationship of the attachment file to attach to the PDF')
|
|
103
|
+
group.add_argument('--pdf-identifier', help='PDF file identifier')
|
|
104
|
+
group.add_argument('--pdf-variant', choices=VARIANTS, help='PDF variant to generate')
|
|
105
|
+
group.add_argument('--pdf-version', help='PDF version number')
|
|
106
|
+
group.add_argument('--pdf-forms', action='store_true', help='include PDF forms')
|
|
107
|
+
group.add_argument('--pdf-tags', action='store_true', help='tag PDF for accessibility')
|
|
108
|
+
group.add_argument(
|
|
97
109
|
'--uncompressed-pdf', action='store_true',
|
|
98
110
|
help='do not compress PDF content, mainly for debugging purpose')
|
|
99
|
-
|
|
111
|
+
group.add_argument(
|
|
112
|
+
'--xmp-metadata', action='append',
|
|
113
|
+
help='URL or filename of a file to include into the XMP metadata')
|
|
114
|
+
group.add_argument(
|
|
100
115
|
'--custom-metadata', action='store_true',
|
|
101
116
|
help='include custom HTML meta tags in PDF metadata')
|
|
102
|
-
|
|
117
|
+
group.add_argument(
|
|
103
118
|
'-p', '--presentational-hints', action='store_true',
|
|
104
119
|
help='follow HTML presentational hints')
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
help='include sRGB color profile')
|
|
108
|
-
PARSER.add_argument(
|
|
120
|
+
group.add_argument('--srgb', action='store_true', help='include sRGB color profile')
|
|
121
|
+
group.add_argument(
|
|
109
122
|
'--optimize-images', action='store_true',
|
|
110
123
|
help='optimize size of embedded images with no quality loss')
|
|
111
|
-
|
|
124
|
+
group.add_argument(
|
|
112
125
|
'-j', '--jpeg-quality', type=int,
|
|
113
126
|
help='JPEG quality between 0 (worst) to 95 (best)')
|
|
114
|
-
|
|
127
|
+
group.add_argument(
|
|
128
|
+
'-D', '--dpi', type=int,
|
|
129
|
+
help='set maximum resolution of images embedded in the PDF')
|
|
130
|
+
group.add_argument(
|
|
115
131
|
'--full-fonts', action='store_true',
|
|
116
132
|
help='embed unmodified font files when possible')
|
|
117
|
-
|
|
118
|
-
'--hinting', action='store_true',
|
|
119
|
-
|
|
120
|
-
PARSER.add_argument(
|
|
133
|
+
group.add_argument(
|
|
134
|
+
'--hinting', action='store_true', help='keep hinting information in embedded fonts')
|
|
135
|
+
group.add_argument(
|
|
121
136
|
'-c', '--cache-folder', dest='cache',
|
|
122
137
|
help='store cache on disk instead of memory, folder is '
|
|
123
138
|
'created if needed and cleaned after the PDF is generated')
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
|
|
140
|
+
group = PARSER.add_argument_group('HTML options')
|
|
141
|
+
group.add_argument('-e', '--encoding', help='force the input character encoding')
|
|
142
|
+
group.add_argument(
|
|
143
|
+
'-m', '--media-type', help='media type to use for @media, defaults to print',
|
|
144
|
+
default='print')
|
|
145
|
+
group.add_argument(
|
|
146
|
+
'-u', '--base-url',
|
|
147
|
+
help='base for relative URLs in the HTML input, defaults to the '
|
|
148
|
+
'input’s own filename or URL or the current directory for stdin')
|
|
149
|
+
|
|
150
|
+
group = PARSER.add_argument_group('URL fetcher options')
|
|
151
|
+
group.add_argument(
|
|
152
|
+
'-t', '--timeout', type=int, help='set timeout in seconds for HTTP requests')
|
|
153
|
+
group.add_argument(
|
|
154
|
+
'--allowed-protocols', dest='allowed_protocols',
|
|
155
|
+
help='only authorize comma-separated list of protocols for fetching URLs')
|
|
156
|
+
group.add_argument(
|
|
157
|
+
'--no-http-redirects', action='store_true', help='do not follow HTTP redirects')
|
|
158
|
+
group.add_argument(
|
|
159
|
+
'--fail-on-http-errors', action='store_true',
|
|
160
|
+
help='abort document rendering on any HTTP error')
|
|
161
|
+
|
|
162
|
+
group = PARSER.add_argument_group('command-line logging options')
|
|
163
|
+
group.add_argument(
|
|
128
164
|
'-v', '--verbose', action='store_true',
|
|
129
165
|
help='show warnings and information messages')
|
|
130
|
-
|
|
166
|
+
group.add_argument(
|
|
131
167
|
'-d', '--debug', action='store_true', help='show debugging messages')
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
PARSER.add_argument(
|
|
135
|
-
'--version', action='version',
|
|
136
|
-
version=f'WeasyPrint version {__version__}',
|
|
137
|
-
help='print WeasyPrint’s version number and exit')
|
|
138
|
-
PARSER.add_argument(
|
|
139
|
-
'-i', '--info', action=PrintInfo, nargs=0,
|
|
140
|
-
help='print system information and exit')
|
|
141
|
-
PARSER.add_argument(
|
|
142
|
-
'-t', '--timeout', type=int,
|
|
143
|
-
help='set timeout in seconds for HTTP requests')
|
|
144
|
-
PARSER.add_argument(
|
|
145
|
-
'--allowed-protocols', dest='allowed_protocols',
|
|
146
|
-
help='only authorize comma-separated list of protocols for fetching URLs')
|
|
168
|
+
group.add_argument('-q', '--quiet', action='store_true', help='hide logging messages')
|
|
169
|
+
|
|
147
170
|
PARSER.set_defaults(**DEFAULT_OPTIONS)
|
|
148
171
|
|
|
149
172
|
|
|
@@ -171,13 +194,17 @@ def main(argv=None, stdout=None, stdin=None, HTML=HTML): # noqa: N803
|
|
|
171
194
|
else:
|
|
172
195
|
output = args.output
|
|
173
196
|
|
|
174
|
-
|
|
197
|
+
fetcher_args = {}
|
|
175
198
|
if args.timeout is not None:
|
|
176
|
-
|
|
199
|
+
fetcher_args['timeout'] = args.timeout
|
|
177
200
|
if args.allowed_protocols is not None:
|
|
178
|
-
|
|
201
|
+
fetcher_args['allowed_protocols'] = {
|
|
179
202
|
protocol.strip().lower() for protocol in args.allowed_protocols.split(',')}
|
|
180
|
-
|
|
203
|
+
if args.no_http_redirects:
|
|
204
|
+
fetcher_args['allow_redirects'] = False
|
|
205
|
+
if args.fail_on_http_errors:
|
|
206
|
+
fetcher_args['fail_on_errors'] = True
|
|
207
|
+
url_fetcher = URLFetcher(**fetcher_args)
|
|
181
208
|
|
|
182
209
|
options = {
|
|
183
210
|
key: value for key, value in vars(args).items() if key in DEFAULT_OPTIONS}
|
weasyprint/css/__init__.py
CHANGED
|
@@ -14,7 +14,6 @@ on other functions in this module.
|
|
|
14
14
|
|
|
15
15
|
import math
|
|
16
16
|
from collections import namedtuple
|
|
17
|
-
from io import BytesIO
|
|
18
17
|
from itertools import groupby
|
|
19
18
|
from logging import DEBUG, WARNING
|
|
20
19
|
from math import inf
|
|
@@ -295,11 +294,10 @@ def find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url,
|
|
|
295
294
|
if href is not None:
|
|
296
295
|
try:
|
|
297
296
|
yield CSS(
|
|
298
|
-
url=href, url_fetcher=url_fetcher,
|
|
299
|
-
_check_mime_type=True, media_type=device_media_type,
|
|
297
|
+
url=href, url_fetcher=url_fetcher, media_type=device_media_type,
|
|
300
298
|
font_config=font_config, counter_style=counter_style,
|
|
301
299
|
color_profiles=color_profiles, page_rules=page_rules,
|
|
302
|
-
layers=layers)
|
|
300
|
+
layers=layers, _check_mime_type=True)
|
|
303
301
|
except URLFetchingError as exception:
|
|
304
302
|
LOGGER.error('Failed to load stylesheet at %s: %s', href, exception)
|
|
305
303
|
LOGGER.debug('Error while loading stylesheet:', exc_info=exception)
|
|
@@ -781,7 +779,6 @@ def resolve_math(token, computed=None, property_name=None, refer_to=None):
|
|
|
781
779
|
return
|
|
782
780
|
|
|
783
781
|
args = []
|
|
784
|
-
original_token = token
|
|
785
782
|
function = Function(token)
|
|
786
783
|
if function.name is None:
|
|
787
784
|
return
|
|
@@ -797,7 +794,7 @@ def resolve_math(token, computed=None, property_name=None, refer_to=None):
|
|
|
797
794
|
if function.name == 'calc':
|
|
798
795
|
result = _resolve_calc_sum(computed, args[0], property_name, refer_to)
|
|
799
796
|
if result is None:
|
|
800
|
-
return
|
|
797
|
+
return
|
|
801
798
|
else:
|
|
802
799
|
return tokenize(result)
|
|
803
800
|
|
|
@@ -1196,10 +1193,10 @@ class ComputedStyle(dict):
|
|
|
1196
1193
|
try:
|
|
1197
1194
|
token = resolve_math(function, self, key)
|
|
1198
1195
|
except PercentageInMath:
|
|
1199
|
-
token = None
|
|
1200
|
-
if token is None:
|
|
1201
1196
|
solved_tokens.append(function)
|
|
1202
1197
|
else:
|
|
1198
|
+
if token is None:
|
|
1199
|
+
raise Exception
|
|
1203
1200
|
solved_tokens.append(token)
|
|
1204
1201
|
original_key = key.replace('_', '-')
|
|
1205
1202
|
value = validate_non_shorthand(solved_tokens, original_key)[0][1]
|
|
@@ -1645,13 +1642,9 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
|
|
|
1645
1642
|
rule.source_column)
|
|
1646
1643
|
continue
|
|
1647
1644
|
|
|
1648
|
-
with fetch(url_fetcher, descriptors['src'][1]) as
|
|
1649
|
-
if 'string' in result:
|
|
1650
|
-
file_object = BytesIO(result['string'])
|
|
1651
|
-
else:
|
|
1652
|
-
file_object = result['file_obj']
|
|
1645
|
+
with fetch(url_fetcher, descriptors['src'][1]) as response:
|
|
1653
1646
|
try:
|
|
1654
|
-
color_profile = ColorProfile(
|
|
1647
|
+
color_profile = ColorProfile(response, descriptors)
|
|
1655
1648
|
except BaseException:
|
|
1656
1649
|
LOGGER.warning(
|
|
1657
1650
|
'Invalid profile file for profile named %r, the whole '
|
weasyprint/css/functions.py
CHANGED
|
@@ -68,6 +68,11 @@ def check_attr(token, allowed_type=None):
|
|
|
68
68
|
name_and_type, fallback = parts[0], ''
|
|
69
69
|
elif len(parts) == 2:
|
|
70
70
|
name_and_type, fallback = parts
|
|
71
|
+
# TODO: support fallbacks with multiple tokens and follow type.
|
|
72
|
+
if len(fallback) >= 1 and fallback[0].type == 'string':
|
|
73
|
+
fallback = fallback[0].value
|
|
74
|
+
else:
|
|
75
|
+
fallback = ''
|
|
71
76
|
else:
|
|
72
77
|
return
|
|
73
78
|
|
weasyprint/css/html5_ua.css
CHANGED
|
@@ -124,7 +124,7 @@ dir, menu, ul { list-style-type: disc }
|
|
|
124
124
|
:is(dir, menu, ol, ul) ul { list-style-type: circle }
|
|
125
125
|
:is(dir, menu, ol, ul) :is(dir, menu, ol, ul) ul { list-style-type: square }
|
|
126
126
|
|
|
127
|
-
::marker { font-variant-numeric: tabular-nums }
|
|
127
|
+
::marker { font-variant-numeric: tabular-nums; white-space: pre; text-transform: none }
|
|
128
128
|
|
|
129
129
|
[dir=ltr i] { direction: ltr }
|
|
130
130
|
[dir=rtl i] { direction: rtl }
|
weasyprint/css/tokens.py
CHANGED
|
@@ -432,7 +432,10 @@ def get_angle(token):
|
|
|
432
432
|
token = resolve_math(token) or token
|
|
433
433
|
except (PercentageInMath, FontUnitInMath):
|
|
434
434
|
return
|
|
435
|
-
if token.type == '
|
|
435
|
+
if token.type == 'number' and token.value == 0:
|
|
436
|
+
# Legacy syntax: https://drafts.csswg.org/css-values-4/#angles.
|
|
437
|
+
return 0
|
|
438
|
+
elif token.type == 'dimension':
|
|
436
439
|
factor = ANGLE_TO_RADIANS.get(token.unit.lower())
|
|
437
440
|
if factor is not None:
|
|
438
441
|
return token.value * factor
|
|
@@ -137,7 +137,7 @@ def other_colors(token):
|
|
|
137
137
|
def outline_color(token):
|
|
138
138
|
if get_keyword(token) == 'invert':
|
|
139
139
|
return 'currentcolor'
|
|
140
|
-
|
|
140
|
+
elif parse_color(token):
|
|
141
141
|
return token
|
|
142
142
|
|
|
143
143
|
|
|
@@ -161,7 +161,7 @@ def color(token):
|
|
|
161
161
|
result = parse_color(token)
|
|
162
162
|
if result == 'currentcolor':
|
|
163
163
|
return 'inherit'
|
|
164
|
-
|
|
164
|
+
elif result:
|
|
165
165
|
return token
|
|
166
166
|
|
|
167
167
|
|
|
@@ -849,7 +849,7 @@ def font_feature_settings(tokens):
|
|
|
849
849
|
tokens, token = tokens[:-1], tokens[-1]
|
|
850
850
|
if token.type == 'ident':
|
|
851
851
|
value = {'on': 1, 'off': 0}.get(token.value)
|
|
852
|
-
elif number := get_number(token, negative=False):
|
|
852
|
+
elif number := get_number(token, negative=False, integer=True):
|
|
853
853
|
value = number.value
|
|
854
854
|
elif len(tokens) == 1:
|
|
855
855
|
value = 1
|
|
@@ -1211,7 +1211,7 @@ def text_decoration_thickness(token):
|
|
|
1211
1211
|
"""``text-decoration-thickness`` property validation."""
|
|
1212
1212
|
if length := get_length(token, percentage=True):
|
|
1213
1213
|
return length
|
|
1214
|
-
elif keyword := get_keyword(token) in ('auto', 'from-font'):
|
|
1214
|
+
elif (keyword := get_keyword(token)) in ('auto', 'from-font'):
|
|
1215
1215
|
return keyword
|
|
1216
1216
|
|
|
1217
1217
|
|