weasyprint 67.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.
- weasyprint/__init__.py +35 -103
- weasyprint/__main__.py +107 -80
- weasyprint/css/__init__.py +4 -10
- 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 +4 -2
- 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/grid.py +25 -14
- weasyprint/layout/page.py +4 -4
- weasyprint/layout/preferred.py +35 -2
- 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/pdf/stream.py +0 -2
- weasyprint/svg/__init__.py +51 -30
- weasyprint/svg/css.py +21 -4
- weasyprint/svg/defs.py +5 -3
- weasyprint/text/fonts.py +2 -3
- weasyprint/urls.py +272 -96
- {weasyprint-67.0.dist-info → weasyprint-68.0.dist-info}/METADATA +2 -1
- {weasyprint-67.0.dist-info → weasyprint-68.0.dist-info}/RECORD +34 -34
- {weasyprint-67.0.dist-info → weasyprint-68.0.dist-info}/WHEEL +0 -0
- {weasyprint-67.0.dist-info → weasyprint-68.0.dist-info}/entry_points.txt +0 -0
- {weasyprint-67.0.dist-info → weasyprint-68.0.dist-info}/licenses/LICENSE +0 -0
weasyprint/document.py
CHANGED
|
@@ -18,7 +18,7 @@ from .layout import LayoutContext, layout_document
|
|
|
18
18
|
from .logger import PROGRESS_LOGGER
|
|
19
19
|
from .matrix import Matrix
|
|
20
20
|
from .pdf import VARIANTS, generate_pdf
|
|
21
|
-
from .pdf.metadata import
|
|
21
|
+
from .pdf.metadata import DocumentMetadata
|
|
22
22
|
from .text.fonts import FontConfiguration
|
|
23
23
|
|
|
24
24
|
|
|
@@ -87,64 +87,6 @@ class Page:
|
|
|
87
87
|
draw_page(self._page_box, stream)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
class DocumentMetadata:
|
|
91
|
-
"""Meta-information belonging to a whole :class:`Document`.
|
|
92
|
-
|
|
93
|
-
New attributes may be added in future versions of WeasyPrint.
|
|
94
|
-
"""
|
|
95
|
-
def __init__(self, title=None, authors=None, description=None, keywords=None,
|
|
96
|
-
generator=None, created=None, modified=None, attachments=None,
|
|
97
|
-
lang=None, custom=None, generate_rdf_metadata=generate_rdf_metadata):
|
|
98
|
-
#: The title of the document, as a string or :obj:`None`.
|
|
99
|
-
#: Extracted from the ``<title>`` element in HTML
|
|
100
|
-
#: and written to the ``/Title`` info field in PDF.
|
|
101
|
-
self.title = title
|
|
102
|
-
#: The authors of the document, as a list of strings.
|
|
103
|
-
#: (Defaults to the empty list.)
|
|
104
|
-
#: Extracted from the ``<meta name=author>`` elements in HTML
|
|
105
|
-
#: and written to the ``/Author`` info field in PDF.
|
|
106
|
-
self.authors = authors or []
|
|
107
|
-
#: The description of the document, as a string or :obj:`None`.
|
|
108
|
-
#: Extracted from the ``<meta name=description>`` element in HTML
|
|
109
|
-
#: and written to the ``/Subject`` info field in PDF.
|
|
110
|
-
self.description = description
|
|
111
|
-
#: Keywords associated with the document, as a list of strings.
|
|
112
|
-
#: (Defaults to the empty list.)
|
|
113
|
-
#: Extracted from ``<meta name=keywords>`` elements in HTML
|
|
114
|
-
#: and written to the ``/Keywords`` info field in PDF.
|
|
115
|
-
self.keywords = keywords or []
|
|
116
|
-
#: The name of one of the software packages
|
|
117
|
-
#: used to generate the document, as a string or :obj:`None`.
|
|
118
|
-
#: Extracted from the ``<meta name=generator>`` element in HTML
|
|
119
|
-
#: and written to the ``/Creator`` info field in PDF.
|
|
120
|
-
self.generator = generator
|
|
121
|
-
#: The creation date of the document, as a string or :obj:`None`.
|
|
122
|
-
#: Dates are in one of the six formats specified in
|
|
123
|
-
#: `W3C’s profile of ISO 8601 <https://www.w3.org/TR/NOTE-datetime>`_.
|
|
124
|
-
#: Extracted from the ``<meta name=dcterms.created>`` element in HTML
|
|
125
|
-
#: and written to the ``/CreationDate`` info field in PDF.
|
|
126
|
-
self.created = created
|
|
127
|
-
#: The modification date of the document, as a string or :obj:`None`.
|
|
128
|
-
#: Dates are in one of the six formats specified in
|
|
129
|
-
#: `W3C’s profile of ISO 8601 <https://www.w3.org/TR/NOTE-datetime>`_.
|
|
130
|
-
#: Extracted from the ``<meta name=dcterms.modified>`` element in HTML
|
|
131
|
-
#: and written to the ``/ModDate`` info field in PDF.
|
|
132
|
-
self.modified = modified
|
|
133
|
-
#: A list of :class:`attachments <weasyprint.Attachment>`, empty by default.
|
|
134
|
-
#: Extracted from the ``<link rel=attachment>`` elements in HTML
|
|
135
|
-
#: and written to the ``/EmbeddedFiles`` dictionary in PDF.
|
|
136
|
-
self.attachments = attachments or []
|
|
137
|
-
#: Document language as BCP 47 language tags.
|
|
138
|
-
#: Extracted from ``<html lang=lang>`` in HTML.
|
|
139
|
-
self.lang = lang
|
|
140
|
-
#: Custom metadata, as a dict whose keys are the metadata names and
|
|
141
|
-
#: values are the metadata values.
|
|
142
|
-
self.custom = custom or {}
|
|
143
|
-
#: Custom RDF metadata generator, which will replace the default generator.
|
|
144
|
-
#: The function should return bytes containing an RDF XML.
|
|
145
|
-
self.generate_rdf_metadata = generate_rdf_metadata
|
|
146
|
-
|
|
147
|
-
|
|
148
90
|
class DiskCache:
|
|
149
91
|
"""Dict-like storing images content on disk.
|
|
150
92
|
|
|
@@ -197,8 +139,8 @@ class Document:
|
|
|
197
139
|
|
|
198
140
|
Typically obtained from :meth:`HTML.render() <weasyprint.HTML.render>`, but
|
|
199
141
|
can also be instantiated directly with a list of :class:`pages <Page>`, a
|
|
200
|
-
set of :class:`metadata <DocumentMetadata>`, a :
|
|
201
|
-
<weasyprint.
|
|
142
|
+
set of :class:`metadata <DocumentMetadata>`, a :class:`url_fetcher
|
|
143
|
+
<weasyprint.urls.URLFetcher>`, and a :class:`font_config
|
|
202
144
|
<weasyprint.text.fonts.FontConfiguration>`.
|
|
203
145
|
|
|
204
146
|
"""
|
|
@@ -269,9 +211,7 @@ class Document:
|
|
|
269
211
|
#: Contains information that does not belong to a specific page
|
|
270
212
|
#: but to the whole document.
|
|
271
213
|
self.metadata = metadata
|
|
272
|
-
#: A
|
|
273
|
-
#: :func:`weasyprint.default_url_fetcher` called to fetch external
|
|
274
|
-
#: resources such as stylesheets and images. (See :ref:`URL Fetchers`.)
|
|
214
|
+
#: A :class:`weasyprint.urls.URLFetcher` object (see :ref:`URL Fetchers`.)
|
|
275
215
|
self.url_fetcher = url_fetcher
|
|
276
216
|
#: A :obj:`dict` of fonts used by the document. Keys are hashes used to
|
|
277
217
|
#: identify fonts, values are ``Font`` objects.
|
weasyprint/draw/text.py
CHANGED
|
@@ -254,8 +254,10 @@ def draw_first_line(stream, textbox, text_overflow, block_ellipsis, matrix):
|
|
|
254
254
|
tree.append(defs)
|
|
255
255
|
ElementTree.SubElement(
|
|
256
256
|
tree, 'use', attrib={'href': f'#glyph{glyph_id}'})
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
if 'viewBox' not in tree.attrib:
|
|
258
|
+
tree.attrib['viewBox'] = f'0 0 {font.upem} {font.upem}'
|
|
259
|
+
image = SVGImage(tree, None, None, None)
|
|
260
|
+
a = d = 1
|
|
259
261
|
emojis.append([image, font, a, d, x_advance, 0])
|
|
260
262
|
elif font.png:
|
|
261
263
|
png_data = get_hb_object_data(font.hb_font, 'png', glyph_id)
|
|
@@ -815,7 +815,10 @@ class InlineFlexBox(FlexContainerBox, InlineLevelBox):
|
|
|
815
815
|
|
|
816
816
|
class GridContainerBox(ParentBox):
|
|
817
817
|
"""A box that contains only grid-items."""
|
|
818
|
-
|
|
818
|
+
def __init__(self, element_tag, style, element, children):
|
|
819
|
+
super().__init__(element_tag, style, element, children)
|
|
820
|
+
# TODO: we shouldn’t store this in the box but in the rendering context instead.
|
|
821
|
+
self.advancements = {}
|
|
819
822
|
|
|
820
823
|
|
|
821
824
|
class GridBox(GridContainerBox, BlockLevelBox):
|
|
@@ -84,6 +84,8 @@ def build_formatting_structure(element_tree, style_for, get_image_from_uri,
|
|
|
84
84
|
target_collector, counter_style, footnotes)
|
|
85
85
|
|
|
86
86
|
target_collector.check_pending_targets()
|
|
87
|
+
process_whitespace(box)
|
|
88
|
+
process_text_transform(box)
|
|
87
89
|
|
|
88
90
|
box.is_for_root_element = True
|
|
89
91
|
# If this is changed, maybe update weasy.layout.page.make_margin_boxes()
|
|
@@ -152,9 +154,10 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
|
|
152
154
|
[0], # quote_depth: single integer
|
|
153
155
|
# TODO: define the footnote counter where it can be updated by page
|
|
154
156
|
{'footnote': [0]}, # counter_values: name -> stacked/scoped values
|
|
155
|
-
[{'footnote'}] # counter_scopes: element depths -> counter names
|
|
157
|
+
[{'footnote'}], # counter_scopes: element depths -> counter names
|
|
158
|
+
[] # page_groups
|
|
156
159
|
)
|
|
157
|
-
quote_depth, counter_values, counter_scopes = state
|
|
160
|
+
quote_depth, counter_values, counter_scopes, _page_groups = state
|
|
158
161
|
|
|
159
162
|
update_counters(state, style)
|
|
160
163
|
|
|
@@ -224,10 +227,8 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
|
|
224
227
|
counter_values.pop(name)
|
|
225
228
|
|
|
226
229
|
box.children = children
|
|
227
|
-
process_whitespace(box)
|
|
228
230
|
set_content_lists(
|
|
229
231
|
element, box, style, counter_values, target_collector, counter_style)
|
|
230
|
-
process_text_transform(box)
|
|
231
232
|
|
|
232
233
|
if marker_boxes and len(box.children) == 1:
|
|
233
234
|
# See https://www.w3.org/TR/css-lists-3/#list-style-position-outside
|
|
@@ -278,7 +279,7 @@ def before_after_to_box(element, pseudo_type, state, style_for,
|
|
|
278
279
|
return []
|
|
279
280
|
box = make_box(f'{element.tag}::{pseudo_type}', style, [], element)
|
|
280
281
|
|
|
281
|
-
quote_depth, counter_values, _counter_scopes = state
|
|
282
|
+
quote_depth, counter_values, _counter_scopes, _page_groups = state
|
|
282
283
|
update_counters(state, style)
|
|
283
284
|
|
|
284
285
|
children = []
|
|
@@ -297,7 +298,7 @@ def before_after_to_box(element, pseudo_type, state, style_for,
|
|
|
297
298
|
|
|
298
299
|
# calculate the bookmark-label
|
|
299
300
|
if style['bookmark_level'] != 'none':
|
|
300
|
-
_quote_depth, counter_values, _counter_scopes = state
|
|
301
|
+
_quote_depth, counter_values, _counter_scopes, _page_groups = state
|
|
301
302
|
compute_bookmark_label(
|
|
302
303
|
element, box, style['bookmark_label'], counter_values,
|
|
303
304
|
target_collector, counter_style)
|
|
@@ -318,7 +319,7 @@ def marker_to_box(element, state, parent_style, style_for, get_image_from_uri,
|
|
|
318
319
|
# TODO: should be the computed value. When does the used value for
|
|
319
320
|
# `display` differ from the computer value? It's at least wrong for
|
|
320
321
|
# `content` where 'normal' computes as 'inhibit' for pseudo elements.
|
|
321
|
-
quote_depth, counter_values, _counter_scopes = state
|
|
322
|
+
quote_depth, counter_values, _counter_scopes, _page_groups = state
|
|
322
323
|
|
|
323
324
|
box = make_box(f'{element.tag}::marker', style, children, element)
|
|
324
325
|
|
|
@@ -423,9 +424,7 @@ def compute_content_list(content_list, parent_box, counter_values, css_token,
|
|
|
423
424
|
boxes.InlineReplacedBox.anonymous_from(parent_box, image))
|
|
424
425
|
elif type_ == 'content()':
|
|
425
426
|
added_text = extract_text(value, parent_box)
|
|
426
|
-
|
|
427
|
-
# (normally done during the layout).
|
|
428
|
-
add_text(added_text.strip())
|
|
427
|
+
add_text(added_text)
|
|
429
428
|
elif type_ == 'string()':
|
|
430
429
|
if not in_page_context:
|
|
431
430
|
# string() is currently only valid in @page context.
|
|
@@ -491,9 +490,7 @@ def compute_content_list(content_list, parent_box, counter_values, css_token,
|
|
|
491
490
|
# TODO: 'before'- and 'after'- content referring missing
|
|
492
491
|
# counters are not properly set.
|
|
493
492
|
text = extract_text(text_style, target_box)
|
|
494
|
-
|
|
495
|
-
# (normally done during the layout)
|
|
496
|
-
add_text(text.strip())
|
|
493
|
+
add_text(text)
|
|
497
494
|
else:
|
|
498
495
|
break
|
|
499
496
|
elif type_ == 'quote' and None not in (quote_depth, quote_style):
|
|
@@ -598,7 +595,6 @@ def compute_string_set(element, box, string_name, content_list,
|
|
|
598
595
|
"""Parse the content-list value of ``string_name`` for ``string-set``."""
|
|
599
596
|
def parse_again(mixin_pagebased_counters=None):
|
|
600
597
|
"""Closure to parse the string-set string value all again."""
|
|
601
|
-
|
|
602
598
|
# Neither alters the mixed-in nor the cached counter values, no
|
|
603
599
|
# need to deepcopy here
|
|
604
600
|
if mixin_pagebased_counters is None:
|
|
@@ -606,7 +602,6 @@ def compute_string_set(element, box, string_name, content_list,
|
|
|
606
602
|
else:
|
|
607
603
|
local_counters = mixin_pagebased_counters.copy()
|
|
608
604
|
local_counters.update(box.cached_counter_values)
|
|
609
|
-
|
|
610
605
|
compute_string_set(
|
|
611
606
|
element, box, string_name, content_list, local_counters,
|
|
612
607
|
target_collector, counter_style)
|
|
@@ -630,7 +625,7 @@ def compute_string_set(element, box, string_name, content_list,
|
|
|
630
625
|
def compute_bookmark_label(element, box, content_list, counter_values,
|
|
631
626
|
target_collector, counter_style):
|
|
632
627
|
"""Parses the content-list value for ``bookmark-label``."""
|
|
633
|
-
def parse_again(mixin_pagebased_counters=
|
|
628
|
+
def parse_again(mixin_pagebased_counters=None):
|
|
634
629
|
"""Closure to parse the bookmark-label all again."""
|
|
635
630
|
# Neither alters the mixed-in nor the cached counter values, no
|
|
636
631
|
# need to deepcopy here
|
|
@@ -638,7 +633,6 @@ def compute_bookmark_label(element, box, content_list, counter_values,
|
|
|
638
633
|
local_counters = {}
|
|
639
634
|
else:
|
|
640
635
|
local_counters = mixin_pagebased_counters.copy()
|
|
641
|
-
local_counters = mixin_pagebased_counters.copy()
|
|
642
636
|
local_counters.update(box.cached_counter_values)
|
|
643
637
|
compute_bookmark_label(
|
|
644
638
|
element, box, content_list, local_counters, target_collector,
|
|
@@ -662,7 +656,7 @@ def set_content_lists(element, box, style, counter_values, target_collector,
|
|
|
662
656
|
"""
|
|
663
657
|
box.string_set = []
|
|
664
658
|
if style['string_set'] != 'none':
|
|
665
|
-
for
|
|
659
|
+
for string_name, string_values in style['string_set']:
|
|
666
660
|
compute_string_set(
|
|
667
661
|
element, box, string_name, string_values, counter_values,
|
|
668
662
|
target_collector, counter_style)
|
|
@@ -674,7 +668,7 @@ def set_content_lists(element, box, style, counter_values, target_collector,
|
|
|
674
668
|
|
|
675
669
|
def update_counters(state, style):
|
|
676
670
|
"""Handle the ``counter-*`` properties."""
|
|
677
|
-
_quote_depth, counter_values, counter_scopes = state
|
|
671
|
+
_quote_depth, counter_values, counter_scopes, _page_groups = state
|
|
678
672
|
sibling_scopes = counter_scopes[-1]
|
|
679
673
|
|
|
680
674
|
for name, value in style['counter_reset']:
|
|
@@ -1113,45 +1107,120 @@ def process_whitespace(box, following_collapsible_space=False):
|
|
|
1113
1107
|
|
|
1114
1108
|
else:
|
|
1115
1109
|
for child in box.children:
|
|
1110
|
+
child_collapsible_space = process_whitespace(
|
|
1111
|
+
child, following_collapsible_space)
|
|
1116
1112
|
if isinstance(child, (boxes.TextBox, boxes.InlineBox)):
|
|
1117
|
-
|
|
1118
|
-
child, following_collapsible_space)
|
|
1119
|
-
if box.is_in_normal_flow() and child.is_in_normal_flow():
|
|
1120
|
-
following_collapsible_space = child_collapsible_space
|
|
1113
|
+
following_collapsible_space = child_collapsible_space
|
|
1121
1114
|
elif child.is_in_normal_flow():
|
|
1122
1115
|
following_collapsible_space = False
|
|
1123
1116
|
|
|
1124
|
-
return following_collapsible_space
|
|
1117
|
+
return following_collapsible_space
|
|
1125
1118
|
|
|
1126
1119
|
|
|
1127
1120
|
def process_text_transform(box):
|
|
1121
|
+
# Rules defined in
|
|
1122
|
+
# https://www.unicode.org/versions/latest/core-spec/chapter-3/#G33992
|
|
1123
|
+
# https://www.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt
|
|
1124
|
+
# https://w3c.github.io/i18n-tests/results/text-transform
|
|
1125
|
+
# Common transformations should be handled by common algorithm in Python, special
|
|
1126
|
+
# casing and tailoring shoud be done here when it depends on the language and not on
|
|
1127
|
+
# only on the glyphs.
|
|
1128
1128
|
if isinstance(box, boxes.TextBox):
|
|
1129
1129
|
text_transform = box.style['text_transform']
|
|
1130
|
+
lang_code = (box.style['lang'] or '').split('-')[0].lower()
|
|
1130
1131
|
if text_transform != 'none':
|
|
1131
1132
|
box.text = {
|
|
1132
|
-
'uppercase':
|
|
1133
|
-
'lowercase':
|
|
1133
|
+
'uppercase': uppercase,
|
|
1134
|
+
'lowercase': lowercase,
|
|
1134
1135
|
'capitalize': capitalize,
|
|
1135
|
-
'full-width': lambda text: text.translate(ASCII_TO_WIDE),
|
|
1136
|
-
}[text_transform](box.text)
|
|
1136
|
+
'full-width': lambda text, lang_code: text.translate(ASCII_TO_WIDE),
|
|
1137
|
+
}[text_transform](box.text, lang_code)
|
|
1137
1138
|
if box.style['hyphens'] == 'none':
|
|
1138
1139
|
box.text = box.text.replace('\u00AD', '') # U+00AD is soft hyphen
|
|
1139
1140
|
|
|
1140
1141
|
elif not box.is_running():
|
|
1141
1142
|
for child in box.children:
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1143
|
+
process_text_transform(child)
|
|
1144
|
+
|
|
1145
|
+
def uppercase(text, lang_code):
|
|
1146
|
+
mapper = {}
|
|
1147
|
+
|
|
1148
|
+
if lang_code == 'el':
|
|
1149
|
+
# https://w3c.github.io/i18n-tests/css-text/text-transform/
|
|
1150
|
+
# text-transform-tailoring-003.html
|
|
1151
|
+
# https://en.wikiversity.org/wiki/Greek_Language/Diphthongs
|
|
1152
|
+
mapper = {
|
|
1153
|
+
'άι': 'ΑΪ',
|
|
1154
|
+
'άυ': 'ΑΫ',
|
|
1155
|
+
'όι': 'ΟΪ',
|
|
1156
|
+
'όυ': 'ΟΫ',
|
|
1157
|
+
'έυ': 'ΗΫ',
|
|
1158
|
+
}
|
|
1159
|
+
elif lang_code in ('tr', 'az'):
|
|
1160
|
+
# https://github.com/unicode-org/cldr/blob/main/common/transforms/tr-Upper.xml
|
|
1161
|
+
mapper = {
|
|
1162
|
+
'i': 'İ',
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
for key, value in mapper.items():
|
|
1166
|
+
text = text.replace(key, value)
|
|
1167
|
+
|
|
1168
|
+
if lang_code == 'el':
|
|
1169
|
+
# Remove diacritics in Greek.
|
|
1170
|
+
# https://github.com/unicode-org/cldr/blob/main/common/transforms/el-Upper.xml
|
|
1171
|
+
# TODO: we should keep tonos on disjunctive eta.
|
|
1172
|
+
# https://w3c.github.io/i18n-tests/css-text/text-transform/
|
|
1173
|
+
# text-transform-tailoring-005.html
|
|
1174
|
+
text = unicodedata.normalize('NFD', text)
|
|
1175
|
+
for char in '\u0313\u0314\u0301\u0300\u0306\u0342\u0304\u0345':
|
|
1176
|
+
text = text.replace(char, '')
|
|
1177
|
+
text = unicodedata.normalize('NFC', text)
|
|
1178
|
+
|
|
1179
|
+
return text.upper()
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def lowercase(text, lang_code):
|
|
1183
|
+
mapper = {}
|
|
1184
|
+
|
|
1185
|
+
if lang_code in ('tr', 'az'):
|
|
1186
|
+
# https://github.com/unicode-org/cldr/blob/main/common/transforms/tr-Lower.xml
|
|
1187
|
+
mapper = {
|
|
1188
|
+
'I': 'ı',
|
|
1189
|
+
'İ': 'i',
|
|
1190
|
+
}
|
|
1191
|
+
elif lang_code == 'lt':
|
|
1192
|
+
# https://github.com/unicode-org/cldr/blob/main/common/transforms/lt-Lower.xml
|
|
1193
|
+
mapper = {
|
|
1194
|
+
'Ì': 'i̇̀',
|
|
1195
|
+
'Í': 'i̇́',
|
|
1196
|
+
'Ĩ': 'i̇̃',
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
for key, value in mapper.items():
|
|
1200
|
+
text = text.replace(key, value)
|
|
1201
|
+
|
|
1202
|
+
return text.lower()
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def capitalize(text, lang_code):
|
|
1147
1206
|
"""Capitalize words according to CSS’s "text-transform: capitalize"."""
|
|
1148
1207
|
letter_found = False
|
|
1208
|
+
skip_next_letter = False
|
|
1149
1209
|
output = ''
|
|
1150
|
-
for letter in text:
|
|
1210
|
+
for i, letter in enumerate(text):
|
|
1211
|
+
if skip_next_letter:
|
|
1212
|
+
skip_next_letter = False
|
|
1213
|
+
continue
|
|
1151
1214
|
category = unicodedata.category(letter)[0]
|
|
1152
1215
|
if not letter_found and category in ('L', 'N'):
|
|
1153
1216
|
letter_found = True
|
|
1154
|
-
|
|
1217
|
+
if lang_code == 'nl' and text[i:i+2] == 'ij':
|
|
1218
|
+
skip_next_letter = True
|
|
1219
|
+
letter = 'IJ'
|
|
1220
|
+
elif lang_code in ('tr', 'az'):
|
|
1221
|
+
letter = uppercase(letter, lang_code)
|
|
1222
|
+
else:
|
|
1223
|
+
letter = letter.upper()
|
|
1155
1224
|
elif category == 'Z':
|
|
1156
1225
|
letter_found = False
|
|
1157
1226
|
output += letter
|
|
@@ -1448,15 +1517,20 @@ def set_viewport_overflow(root_box):
|
|
|
1448
1517
|
|
|
1449
1518
|
|
|
1450
1519
|
def box_text(box):
|
|
1520
|
+
# Stripping may not be the "right" way, but it seems to be what users usually want
|
|
1521
|
+
# in this case. The specification asks for the "text content", probably as defined
|
|
1522
|
+
# in DOM.
|
|
1523
|
+
box = box.deepcopy()
|
|
1524
|
+
process_whitespace(box)
|
|
1451
1525
|
if isinstance(box, boxes.TextBox):
|
|
1452
|
-
return box.text
|
|
1526
|
+
return box.text.strip()
|
|
1453
1527
|
elif isinstance(box, boxes.ParentBox):
|
|
1454
1528
|
return ''.join(
|
|
1455
1529
|
child.text for child in box.descendants()
|
|
1456
1530
|
if not child.element_tag.endswith('::before') and
|
|
1457
1531
|
not child.element_tag.endswith('::after') and
|
|
1458
1532
|
not child.element_tag.endswith('::marker') and
|
|
1459
|
-
isinstance(child, boxes.TextBox))
|
|
1533
|
+
isinstance(child, boxes.TextBox)).strip()
|
|
1460
1534
|
return ''
|
|
1461
1535
|
|
|
1462
1536
|
|
weasyprint/images.py
CHANGED
|
@@ -30,12 +30,6 @@ class ImageLoadingError(ValueError):
|
|
|
30
30
|
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
@classmethod
|
|
34
|
-
def from_exception(cls, exception):
|
|
35
|
-
name = type(exception).__name__
|
|
36
|
-
value = str(exception)
|
|
37
|
-
return cls(f'{name}: {value}' if value else name)
|
|
38
|
-
|
|
39
33
|
|
|
40
34
|
class RasterImage:
|
|
41
35
|
def __init__(self, pillow_image, image_id, image_data, filename=None,
|
|
@@ -257,7 +251,7 @@ class LazyLocalImage(pydyf.Object):
|
|
|
257
251
|
class SVGImage:
|
|
258
252
|
def __init__(self, tree, base_url, url_fetcher, context):
|
|
259
253
|
font_config = context.font_config if context else None
|
|
260
|
-
self._svg = SVG(tree, base_url, font_config)
|
|
254
|
+
self._svg = SVG(tree, base_url, font_config, url_fetcher)
|
|
261
255
|
self._base_url = base_url
|
|
262
256
|
self._url_fetcher = url_fetcher
|
|
263
257
|
self._context = context
|
|
@@ -284,7 +278,7 @@ class SVGImage:
|
|
|
284
278
|
try:
|
|
285
279
|
self._svg.draw(
|
|
286
280
|
stream, concrete_width, concrete_height, self._base_url,
|
|
287
|
-
self.
|
|
281
|
+
self._context)
|
|
288
282
|
except BaseException as exception:
|
|
289
283
|
LOGGER.error('Failed to render SVG image %s', self._base_url)
|
|
290
284
|
LOGGER.debug('Error while rendering SVG image:', exc_info=exception)
|
|
@@ -297,43 +291,40 @@ def get_image_from_uri(cache, url_fetcher, options, url, forced_mime_type=None,
|
|
|
297
291
|
return cache[url]
|
|
298
292
|
|
|
299
293
|
try:
|
|
300
|
-
with fetch(url_fetcher, url) as
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
else:
|
|
304
|
-
string = result['file_obj'].read()
|
|
305
|
-
mime_type = forced_mime_type or result['mime_type']
|
|
294
|
+
with fetch(url_fetcher, url) as response:
|
|
295
|
+
bytestring = response.read()
|
|
296
|
+
mime_type = forced_mime_type or response.content_type
|
|
306
297
|
|
|
307
298
|
image = None
|
|
308
299
|
svg_exceptions = []
|
|
309
300
|
# Try to rely on given mimetype for SVG
|
|
310
301
|
if mime_type == 'image/svg+xml':
|
|
311
302
|
try:
|
|
312
|
-
tree = ElementTree.fromstring(
|
|
303
|
+
tree = ElementTree.fromstring(bytestring)
|
|
313
304
|
image = SVGImage(tree, url, url_fetcher, context)
|
|
314
305
|
except Exception as svg_exception:
|
|
315
306
|
svg_exceptions.append(svg_exception)
|
|
316
307
|
# Try pillow for raster images, or for failing SVG
|
|
317
308
|
if image is None:
|
|
318
309
|
try:
|
|
319
|
-
pillow_image = Image.open(BytesIO(
|
|
310
|
+
pillow_image = Image.open(BytesIO(bytestring))
|
|
320
311
|
except Exception as raster_exception:
|
|
321
312
|
if mime_type == 'image/svg+xml':
|
|
322
313
|
# Tried SVGImage then Pillow for a SVG, abort
|
|
323
|
-
raise ImageLoadingError
|
|
314
|
+
raise ImageLoadingError from svg_exceptions[0]
|
|
324
315
|
try:
|
|
325
316
|
# Last chance, try SVG
|
|
326
|
-
tree = ElementTree.fromstring(
|
|
317
|
+
tree = ElementTree.fromstring(bytestring)
|
|
327
318
|
image = SVGImage(tree, url, url_fetcher, context)
|
|
328
319
|
except Exception:
|
|
329
320
|
# Tried Pillow then SVGImage for a raster, abort
|
|
330
|
-
raise ImageLoadingError
|
|
321
|
+
raise ImageLoadingError from raster_exception
|
|
331
322
|
else:
|
|
332
323
|
# Store image id to enable cache in Stream.add_image
|
|
333
324
|
image_id = md5(url.encode(), usedforsecurity=False).hexdigest()
|
|
334
|
-
path = result.get('path')
|
|
335
325
|
image = RasterImage(
|
|
336
|
-
pillow_image, image_id,
|
|
326
|
+
pillow_image, image_id, bytestring, response.path, cache,
|
|
327
|
+
orientation, options)
|
|
337
328
|
|
|
338
329
|
except (URLFetchingError, ImageLoadingError) as exception:
|
|
339
330
|
LOGGER.error('Failed to load image at %r: %s', url, exception)
|
|
@@ -655,7 +646,7 @@ class LinearGradient(Gradient):
|
|
|
655
646
|
positions.append(positions[-1] + step)
|
|
656
647
|
last += step * stop_length
|
|
657
648
|
|
|
658
|
-
# Add colors before
|
|
649
|
+
# Add colors before first step
|
|
659
650
|
while first > 0:
|
|
660
651
|
step = next(previous_steps)
|
|
661
652
|
colors.insert(0, next(previous_colors))
|
|
@@ -764,14 +755,15 @@ class RadialGradient(Gradient):
|
|
|
764
755
|
center_x, center_y / scale_y, last)
|
|
765
756
|
|
|
766
757
|
if self.repeating:
|
|
767
|
-
points, positions, colors = self._repeat(
|
|
768
|
-
width, height, scale_y, points, positions, colors)
|
|
758
|
+
points, positions, colors, hints = self._repeat(
|
|
759
|
+
width, height, scale_y, points, positions, colors, hints)
|
|
769
760
|
|
|
770
761
|
return scale_y, 'radial', points, positions, colors, hints
|
|
771
762
|
|
|
772
|
-
def _repeat(self, width, height, scale_y, points, positions, colors):
|
|
763
|
+
def _repeat(self, width, height, scale_y, points, positions, colors, hints):
|
|
773
764
|
# Keep original lists and values, they’re useful
|
|
774
765
|
original_colors = colors.copy()
|
|
766
|
+
original_hints = hints.copy()
|
|
775
767
|
original_positions = positions.copy()
|
|
776
768
|
gradient_length = points[5] - points[2]
|
|
777
769
|
|
|
@@ -787,13 +779,14 @@ class RadialGradient(Gradient):
|
|
|
787
779
|
# Repeat colors and extrapolate positions
|
|
788
780
|
repeat = 1 + repeat_after
|
|
789
781
|
colors *= repeat
|
|
782
|
+
hints = ([*hints, 1] * repeat)[:-1]
|
|
790
783
|
positions = [
|
|
791
784
|
i + position for i in range(repeat) for position in positions]
|
|
792
785
|
points = (*points[:5], points[5] + gradient_length * repeat_after)
|
|
793
786
|
|
|
794
787
|
if points[2] == 0:
|
|
795
788
|
# Inner circle has 0 radius, no need to repeat inside, return
|
|
796
|
-
return points, positions, colors
|
|
789
|
+
return points, positions, colors, hints
|
|
797
790
|
|
|
798
791
|
# Find how many times we have to repeat the colors inside
|
|
799
792
|
repeat_before = points[2] / gradient_length
|
|
@@ -806,6 +799,7 @@ class RadialGradient(Gradient):
|
|
|
806
799
|
if full_repeat:
|
|
807
800
|
# Repeat colors and extrapolate positions
|
|
808
801
|
colors += original_colors * full_repeat
|
|
802
|
+
hints += [1, *original_hints] * full_repeat
|
|
809
803
|
positions = [
|
|
810
804
|
i - full_repeat + position for i in range(full_repeat)
|
|
811
805
|
for position in original_positions] + positions
|
|
@@ -814,7 +808,7 @@ class RadialGradient(Gradient):
|
|
|
814
808
|
partial_repeat = repeat_before - full_repeat
|
|
815
809
|
if partial_repeat == 0:
|
|
816
810
|
# No partial repeat, return
|
|
817
|
-
return points, positions, colors
|
|
811
|
+
return points, positions, colors, hints
|
|
818
812
|
|
|
819
813
|
# Iterate through positions in reverse order, from the outer
|
|
820
814
|
# circle to the original inner circle, to find positions from
|
|
@@ -828,11 +822,12 @@ class RadialGradient(Gradient):
|
|
|
828
822
|
# The center is a color of the gradient, truncate original
|
|
829
823
|
# colors and positions and prepend them
|
|
830
824
|
colors = original_colors[-i:] + colors
|
|
825
|
+
hints = [*original_hints[-i:], 1, *hints]
|
|
831
826
|
new_positions = [
|
|
832
827
|
position - full_repeat - 1
|
|
833
828
|
for position in original_positions[-i:]]
|
|
834
829
|
positions = new_positions + positions
|
|
835
|
-
return points, positions, colors
|
|
830
|
+
return points, positions, colors, hints
|
|
836
831
|
if position < ratio:
|
|
837
832
|
# The center is between two colors of the gradient,
|
|
838
833
|
# define the center color as the average of these two
|
|
@@ -842,14 +837,14 @@ class RadialGradient(Gradient):
|
|
|
842
837
|
next_position = original_positions[-(i - 1)]
|
|
843
838
|
average_colors = [color, color, next_color, next_color]
|
|
844
839
|
average_positions = [position, ratio, ratio, next_position]
|
|
845
|
-
zero_color = gradient_average_color(
|
|
846
|
-
|
|
847
|
-
|
|
840
|
+
zero_color = gradient_average_color(average_colors, average_positions)
|
|
841
|
+
colors = [zero_color, *original_colors[-(i-1):], *colors]
|
|
842
|
+
hints = [1, *original_hints[-(i-1):], 1, *hints]
|
|
848
843
|
new_positions = [
|
|
849
844
|
position - 1 - full_repeat for position
|
|
850
845
|
in original_positions[-(i - 1):]]
|
|
851
846
|
positions = (ratio - 1 - full_repeat, *new_positions, *positions)
|
|
852
|
-
return points, positions, colors
|
|
847
|
+
return points, positions, colors, hints
|
|
853
848
|
|
|
854
849
|
def _resolve_size(self, width, height, center_x, center_y, style):
|
|
855
850
|
"""Resolve circle size of the radial gradient."""
|
weasyprint/layout/__init__.py
CHANGED