weasyprint 66.0__py3-none-any.whl → 68.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.
Files changed (65) hide show
  1. weasyprint/__init__.py +47 -108
  2. weasyprint/__main__.py +120 -84
  3. weasyprint/anchors.py +4 -4
  4. weasyprint/css/__init__.py +719 -68
  5. weasyprint/css/computed_values.py +64 -175
  6. weasyprint/css/counters.py +1 -1
  7. weasyprint/css/functions.py +211 -0
  8. weasyprint/css/html5_ua.css +2 -1
  9. weasyprint/css/html5_ua_form.css +1 -1
  10. weasyprint/css/media_queries.py +3 -1
  11. weasyprint/css/properties.py +6 -2
  12. weasyprint/css/{utils.py → tokens.py} +310 -398
  13. weasyprint/css/units.py +91 -0
  14. weasyprint/css/validation/__init__.py +1 -1
  15. weasyprint/css/validation/descriptors.py +47 -19
  16. weasyprint/css/validation/expanders.py +7 -8
  17. weasyprint/css/validation/properties.py +343 -359
  18. weasyprint/document.py +22 -73
  19. weasyprint/draw/__init__.py +6 -7
  20. weasyprint/draw/border.py +3 -5
  21. weasyprint/draw/color.py +1 -1
  22. weasyprint/draw/text.py +62 -40
  23. weasyprint/formatting_structure/boxes.py +24 -3
  24. weasyprint/formatting_structure/build.py +113 -41
  25. weasyprint/images.py +94 -78
  26. weasyprint/layout/__init__.py +29 -25
  27. weasyprint/layout/absolute.py +3 -5
  28. weasyprint/layout/background.py +7 -7
  29. weasyprint/layout/block.py +140 -128
  30. weasyprint/layout/column.py +18 -24
  31. weasyprint/layout/flex.py +13 -5
  32. weasyprint/layout/float.py +4 -6
  33. weasyprint/layout/grid.py +304 -99
  34. weasyprint/layout/inline.py +114 -60
  35. weasyprint/layout/page.py +27 -16
  36. weasyprint/layout/percent.py +14 -10
  37. weasyprint/layout/preferred.py +79 -31
  38. weasyprint/layout/replaced.py +9 -6
  39. weasyprint/layout/table.py +8 -5
  40. weasyprint/pdf/__init__.py +58 -14
  41. weasyprint/pdf/anchors.py +11 -18
  42. weasyprint/pdf/fonts.py +135 -69
  43. weasyprint/pdf/metadata.py +155 -68
  44. weasyprint/pdf/pdfa.py +20 -6
  45. weasyprint/pdf/pdfua.py +1 -3
  46. weasyprint/pdf/pdfx.py +81 -0
  47. weasyprint/pdf/stream.py +18 -3
  48. weasyprint/pdf/tags.py +6 -4
  49. weasyprint/svg/__init__.py +85 -48
  50. weasyprint/svg/css.py +21 -4
  51. weasyprint/svg/defs.py +5 -3
  52. weasyprint/svg/images.py +11 -3
  53. weasyprint/svg/text.py +11 -2
  54. weasyprint/svg/utils.py +6 -3
  55. weasyprint/text/constants.py +1 -1
  56. weasyprint/text/ffi.py +4 -3
  57. weasyprint/text/fonts.py +14 -7
  58. weasyprint/text/line_break.py +101 -17
  59. weasyprint/urls.py +288 -95
  60. {weasyprint-66.0.dist-info → weasyprint-68.0.dist-info}/METADATA +6 -6
  61. weasyprint-68.0.dist-info/RECORD +77 -0
  62. weasyprint-66.0.dist-info/RECORD +0 -74
  63. {weasyprint-66.0.dist-info → weasyprint-68.0.dist-info}/WHEEL +0 -0
  64. {weasyprint-66.0.dist-info → weasyprint-68.0.dist-info}/entry_points.txt +0 -0
  65. {weasyprint-66.0.dist-info → weasyprint-68.0.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__ = '66.0'
17
+ VERSION = __version__ = '68.0'
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__ = '66.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 ( # noqa: I001, E402
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
- A function or other callable with the same signature as
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=default_url_fetcher, media_type='print'):
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
- result = _select_source(
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 (source_type, source, base_url, protocol_encoding):
174
- if isinstance(source, str):
175
- result = tinyhtml5.parse(source, namespace_html_elements=False)
176
- else:
177
- kwargs = {'namespace_html_elements': False}
178
- if protocol_encoding is not None:
179
- kwargs['transport_encoding'] = protocol_encoding
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
@@ -198,7 +194,8 @@ class HTML:
198
194
  def _ph_stylesheets(self):
199
195
  return [HTML5_PH_STYLESHEET]
200
196
 
201
- def render(self, font_config=None, counter_style=None, **options):
197
+ def render(self, font_config=None, counter_style=None, color_profiles=None,
198
+ **options):
202
199
  """Lay out and paginate the document, but do not (yet) export it.
203
200
 
204
201
  This returns a :class:`document.Document` object which provides
@@ -222,10 +219,11 @@ class HTML:
222
219
  new_options = DEFAULT_OPTIONS.copy()
223
220
  new_options.update(options)
224
221
  options = new_options
225
- return Document._render(self, font_config, counter_style, options)
222
+ return Document._render(
223
+ self, font_config, counter_style, color_profiles, options)
226
224
 
227
225
  def write_pdf(self, target=None, zoom=1, finisher=None,
228
- font_config=None, counter_style=None, **options):
226
+ font_config=None, counter_style=None, color_profiles=None, **options):
229
227
  """Render the document to a PDF file.
230
228
 
231
229
  This is a shortcut for calling :meth:`render`, then
@@ -265,7 +263,7 @@ class HTML:
265
263
  new_options.update(options)
266
264
  options = new_options
267
265
  return (
268
- self.render(font_config, counter_style, **options)
266
+ self.render(font_config, counter_style, color_profiles, **options)
269
267
  .write_pdf(target, zoom, finisher, **options))
270
268
 
271
269
 
@@ -284,35 +282,37 @@ class CSS:
284
282
  of :class:`HTML` objects.
285
283
 
286
284
  """
287
- def __init__(self, guess=None, filename=None, url=None, file_obj=None,
288
- string=None, encoding=None, base_url=None,
289
- 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,
290
287
  media_type='print', font_config=None, counter_style=None,
291
- matcher=None, page_rules=None):
288
+ color_profiles=None, matcher=None, page_rules=None, layers=None,
289
+ layer=None):
292
290
  PROGRESS_LOGGER.info(
293
291
  'Step 2 - Fetching and parsing CSS - %s',
294
292
  filename or url or getattr(file_obj, 'name', 'CSS string'))
295
- result = _select_source(
296
- guess, filename, url, file_obj, string,
297
- base_url=base_url, url_fetcher=url_fetcher,
298
- check_css_mime_type=_check_mime_type)
299
- with result as (source_type, source, base_url, protocol_encoding):
300
- if source_type == 'file_obj':
301
- source = source.read()
302
- if isinstance(source, str):
303
- # unicode, no encoding
304
- 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)
305
302
  else:
306
- stylesheet, encoding = tinycss2.parse_stylesheet_bytes(
307
- source, environment_encoding=encoding,
303
+ stylesheet, _ = tinycss2.parse_stylesheet_bytes(
304
+ css, environment_encoding=encoding,
308
305
  protocol_encoding=protocol_encoding)
309
306
  self.base_url = base_url
310
307
  self.matcher = matcher or cssselect2.Matcher()
311
308
  self.page_rules = [] if page_rules is None else page_rules
309
+ self.layers = [] if layers is None else layers
312
310
  counter_style = {} if counter_style is None else counter_style
311
+ color_profiles = {} if color_profiles is None else color_profiles
313
312
  preprocess_stylesheet(
314
313
  media_type, base_url, stylesheet, url_fetcher, self.matcher,
315
- self.page_rules, font_config, counter_style)
314
+ self.page_rules, self.layers, font_config, counter_style, color_profiles,
315
+ layer=layer)
316
316
 
317
317
 
318
318
  class Attachment:
@@ -341,10 +341,12 @@ class Attachment:
341
341
 
342
342
  """
343
343
  def __init__(self, guess=None, filename=None, url=None, file_obj=None,
344
- string=None, base_url=None, url_fetcher=default_url_fetcher,
345
- name=None, description=None, created=None, modified=None,
344
+ string=None, base_url=None, url_fetcher=None, name=None,
345
+ description=None, created=None, modified=None,
346
346
  relationship='Unspecified'):
347
- self.source = _select_source(
347
+ if url_fetcher is None:
348
+ url_fetcher = URLFetcher()
349
+ self.source = select_source(
348
350
  guess, filename, url, file_obj, string, base_url=base_url,
349
351
  url_fetcher=url_fetcher)
350
352
  self.name = name
@@ -366,69 +368,6 @@ class Attachment:
366
368
  self.modified = modified
367
369
 
368
370
 
369
- @contextlib.contextmanager
370
- def _select_source(guess=None, filename=None, url=None, file_obj=None,
371
- string=None, base_url=None, url_fetcher=default_url_fetcher,
372
- check_css_mime_type=False):
373
- """If only one input is given, return it with normalized ``base_url``."""
374
- if base_url is not None:
375
- base_url = ensure_url(base_url)
376
-
377
- selected_params = [
378
- param for param in (guess, filename, url, file_obj, string) if
379
- param is not None]
380
- if len(selected_params) != 1:
381
- source = ', '.join(selected_params) or 'nothing'
382
- raise TypeError(f'Expected exactly one source, got {source}')
383
- elif guess is not None:
384
- if hasattr(guess, 'read'):
385
- type_ = 'file_obj'
386
- elif isinstance(guess, Path):
387
- type_ = 'filename'
388
- elif url_is_absolute(guess):
389
- type_ = 'url'
390
- else:
391
- type_ = 'filename'
392
- result = _select_source(
393
- base_url=base_url, url_fetcher=url_fetcher,
394
- check_css_mime_type=check_css_mime_type,
395
- **{type_: guess})
396
- with result as result:
397
- yield result
398
- elif filename is not None:
399
- if base_url is None:
400
- base_url = path2url(filename)
401
- with open(filename, 'rb') as file_obj:
402
- yield 'file_obj', file_obj, base_url, None
403
- elif url is not None:
404
- with fetch(url_fetcher, url) as result:
405
- if check_css_mime_type and result['mime_type'] != 'text/css':
406
- LOGGER.error(
407
- 'Unsupported stylesheet type %s for %s',
408
- result['mime_type'], result['redirected_url'])
409
- yield 'string', '', base_url, None
410
- else:
411
- proto_encoding = result.get('encoding')
412
- if base_url is None:
413
- base_url = result.get('redirected_url', url)
414
- if 'string' in result:
415
- yield 'string', result['string'], base_url, proto_encoding
416
- else:
417
- yield (
418
- 'file_obj', result['file_obj'], base_url,
419
- proto_encoding)
420
- elif file_obj is not None:
421
- if base_url is None:
422
- # filesystem file-like objects have a 'name' attribute.
423
- name = getattr(file_obj, 'name', None)
424
- # Some streams have a .name like '<stdin>', not a filename.
425
- if name and not name.startswith('<'):
426
- base_url = ensure_url(name)
427
- yield 'file_obj', file_obj, base_url, None
428
- else:
429
- assert string is not None
430
- yield 'string', string, base_url, None
431
-
432
371
  # Work around circular imports.
433
372
  from .css import preprocess_stylesheet # noqa: I001, E402
434
373
  from .html import ( # noqa: E402
weasyprint/__main__.py CHANGED
@@ -4,141 +4,169 @@ 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 default_url_fetcher
13
+ from .urls import URLFetcher
15
14
 
16
15
 
17
16
  class PrintInfo(argparse.Action):
18
17
  def __call__(*_, **__):
18
+ # TODO: ignore check at block-level when available.
19
+ # https://github.com/astral-sh/ruff/issues/3711
19
20
  uname = platform.uname()
20
- print('System:', uname.system)
21
- print('Machine:', uname.machine)
22
- print('Version:', uname.version)
23
- print('Release:', uname.release)
24
- print()
25
- print('WeasyPrint version:', __version__)
26
- print('Python version:', sys.version.split()[0])
27
- print('Pydyf version:', pydyf.__version__)
28
- print('Pango version:', pango.pango_version())
21
+ print('System:', uname.system) # noqa: T201
22
+ print('Machine:', uname.machine) # noqa: T201
23
+ print('Version:', uname.version) # noqa: T201
24
+ print('Release:', uname.release) # noqa: T201
25
+ print() # noqa: T201
26
+ print('WeasyPrint version:', __version__) # noqa: T201
27
+ print('Python version:', sys.version.split()[0]) # noqa: T201
28
+ print('Pydyf version:', pydyf.__version__) # noqa: T201
29
+ print('Pango version:', pango.pango_version()) # noqa: T201
29
30
  sys.exit()
30
31
 
31
32
 
32
33
  class Parser(argparse.ArgumentParser):
33
34
  def __init__(self, *args, **kwargs):
34
- self._arguments = {}
35
+ self._groups = {None: {}}
35
36
  super().__init__(*args, **kwargs)
36
37
 
37
- def add_argument(self, *args, **kwargs):
38
- super().add_argument(*args, **kwargs)
38
+ def add_argument(self, *args, _group_name=None, **kwargs):
39
+ if _group_name is None:
40
+ super().add_argument(*args, **kwargs)
39
41
  key = args[-1].lstrip('-')
40
42
  kwargs['flags'] = args
41
43
  kwargs['positional'] = args[-1][0] != '-'
42
- self._arguments[key] = kwargs
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
43
55
 
44
56
  @property
45
57
  def docstring(self):
46
- self._arguments['help'] = self._arguments.pop('help')
58
+ self._groups[None].pop('help')
47
59
  data = []
48
- for key, args in self._arguments.items():
49
- data.append('.. option:: ')
50
- action = args.get('action', 'store')
51
- for flag in args['flags']:
52
- data.append(flag)
53
- if not args['positional'] and action in ('store', 'append'):
54
- data.append(f' <{key}>')
55
- data.append(', ')
56
- data[-1] = '\n\n'
57
- data.append(f' {args["help"][0].upper()}{args["help"][1:]}.\n\n')
58
- if 'choices' in args:
59
- choices = ", ".join(args['choices'])
60
- data.append(f' Possible choices: {choices}.\n\n')
61
- if action == 'append':
62
- data.append(' This option can be passed multiple times.\n\n')
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')
63
81
  return ''.join(data)
64
82
 
65
83
 
66
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')
67
87
  PARSER.add_argument(
68
- 'input', help='URL or filename of the HTML input, or - for stdin')
69
- PARSER.add_argument(
70
- 'output', help='filename where output is written, or - for stdout')
71
- PARSER.add_argument(
72
- '-e', '--encoding', help='force the input character encoding')
88
+ '-i', '--info', action=PrintInfo, nargs=0, help='print system information and exit')
73
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(
74
95
  '-s', '--stylesheet', action='append', dest='stylesheets',
75
96
  help='URL or filename for a user CSS stylesheet')
76
- PARSER.add_argument(
77
- '-m', '--media-type',
78
- help='media type to use for @media, defaults to print')
79
- PARSER.add_argument(
80
- '-u', '--base-url',
81
- help='base for relative URLs in the HTML input, defaults to the '
82
- 'input’s own filename or URL or the current directory for stdin')
83
- PARSER.add_argument(
97
+ group.add_argument(
84
98
  '-a', '--attachment', action='append', dest='attachments',
85
99
  help='URL or filename of a file to attach to the PDF document')
86
- PARSER.add_argument('--pdf-identifier', help='PDF file identifier')
87
- PARSER.add_argument(
88
- '--pdf-variant', choices=VARIANTS, help='PDF variant to generate')
89
- PARSER.add_argument('--pdf-version', help='PDF version number')
90
- PARSER.add_argument(
91
- '--pdf-forms', action='store_true', help='include PDF forms')
92
- PARSER.add_argument(
93
- '--pdf-tags', action='store_true', help='tag PDF for accessibility')
94
- PARSER.add_argument(
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(
95
109
  '--uncompressed-pdf', action='store_true',
96
110
  help='do not compress PDF content, mainly for debugging purpose')
97
- PARSER.add_argument(
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(
98
115
  '--custom-metadata', action='store_true',
99
116
  help='include custom HTML meta tags in PDF metadata')
100
- PARSER.add_argument(
117
+ group.add_argument(
101
118
  '-p', '--presentational-hints', action='store_true',
102
119
  help='follow HTML presentational hints')
103
- PARSER.add_argument(
104
- '--srgb', action='store_true',
105
- help='include sRGB color profile')
106
- PARSER.add_argument(
120
+ group.add_argument('--srgb', action='store_true', help='include sRGB color profile')
121
+ group.add_argument(
107
122
  '--optimize-images', action='store_true',
108
123
  help='optimize size of embedded images with no quality loss')
109
- PARSER.add_argument(
124
+ group.add_argument(
110
125
  '-j', '--jpeg-quality', type=int,
111
126
  help='JPEG quality between 0 (worst) to 95 (best)')
112
- PARSER.add_argument(
127
+ group.add_argument(
128
+ '-D', '--dpi', type=int,
129
+ help='set maximum resolution of images embedded in the PDF')
130
+ group.add_argument(
113
131
  '--full-fonts', action='store_true',
114
132
  help='embed unmodified font files when possible')
115
- PARSER.add_argument(
116
- '--hinting', action='store_true',
117
- help='keep hinting information in embedded fonts')
118
- PARSER.add_argument(
133
+ group.add_argument(
134
+ '--hinting', action='store_true', help='keep hinting information in embedded fonts')
135
+ group.add_argument(
119
136
  '-c', '--cache-folder', dest='cache',
120
137
  help='store cache on disk instead of memory, folder is '
121
138
  'created if needed and cleaned after the PDF is generated')
122
- PARSER.add_argument(
123
- '-D', '--dpi', type=int,
124
- help='set maximum resolution of images embedded in the PDF')
125
- PARSER.add_argument(
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(
126
164
  '-v', '--verbose', action='store_true',
127
165
  help='show warnings and information messages')
128
- PARSER.add_argument(
166
+ group.add_argument(
129
167
  '-d', '--debug', action='store_true', help='show debugging messages')
130
- PARSER.add_argument(
131
- '-q', '--quiet', action='store_true', help='hide logging messages')
132
- PARSER.add_argument(
133
- '--version', action='version',
134
- version=f'WeasyPrint version {__version__}',
135
- help='print WeasyPrint’s version number and exit')
136
- PARSER.add_argument(
137
- '-i', '--info', action=PrintInfo, nargs=0,
138
- help='print system information and exit')
139
- PARSER.add_argument(
140
- '-t', '--timeout', type=int,
141
- help='Set timeout in seconds for HTTP requests')
168
+ group.add_argument('-q', '--quiet', action='store_true', help='hide logging messages')
169
+
142
170
  PARSER.set_defaults(**DEFAULT_OPTIONS)
143
171
 
144
172
 
@@ -166,9 +194,17 @@ def main(argv=None, stdout=None, stdin=None, HTML=HTML): # noqa: N803
166
194
  else:
167
195
  output = args.output
168
196
 
169
- url_fetcher = default_url_fetcher
197
+ fetcher_args = {}
170
198
  if args.timeout is not None:
171
- url_fetcher = partial(default_url_fetcher, timeout=args.timeout)
199
+ fetcher_args['timeout'] = args.timeout
200
+ if args.allowed_protocols is not None:
201
+ fetcher_args['allowed_protocols'] = {
202
+ protocol.strip().lower() for protocol in args.allowed_protocols.split(',')}
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)
172
208
 
173
209
  options = {
174
210
  key: value for key, value in vars(args).items() if key in DEFAULT_OPTIONS}
weasyprint/anchors.py CHANGED
@@ -43,8 +43,8 @@ def gather_anchors(box, anchors, links, bookmarks, forms, parent_matrix=None,
43
43
  border_width = box.border_width()
44
44
  border_height = box.border_height()
45
45
  origin_x, origin_y = box.style['transform_origin']
46
- offset_x = percentage(origin_x, border_width)
47
- offset_y = percentage(origin_y, border_height)
46
+ offset_x = percentage(origin_x, box.style, border_width)
47
+ offset_y = percentage(origin_y, box.style, border_height)
48
48
  origin_x = box.border_box_x() + offset_x
49
49
  origin_y = box.border_box_y() + offset_y
50
50
 
@@ -58,8 +58,8 @@ def gather_anchors(box, anchors, links, bookmarks, forms, parent_matrix=None,
58
58
  b = math.sin(args)
59
59
  c = -b
60
60
  elif name == 'translate':
61
- e = percentage(args[0], border_width)
62
- f = percentage(args[1], border_height)
61
+ e = percentage(args[0], box.style, border_width)
62
+ f = percentage(args[1], box.style, border_height)
63
63
  elif name == 'skew':
64
64
  b, c = math.tan(args[1]), math.tan(args[0])
65
65
  else: