Codebase list weasyprint / ec7dd1f
New upstream version 51 Scott Kitterman 4 years ago
27 changed file(s) with 535 addition(s) and 118 deletion(s). Raw diff Collapse all Expand all
00 Metadata-Version: 2.1
11 Name: WeasyPrint
2 Version: 50
2 Version: 51
33 Summary: The Awesome Document Factory
44 Home-page: https://weasyprint.org/
55 Author: Simon Sapin
4646 Classifier: Programming Language :: Python :: 3.5
4747 Classifier: Programming Language :: Python :: 3.6
4848 Classifier: Programming Language :: Python :: 3.7
49 Classifier: Programming Language :: Python :: 3.8
4950 Classifier: Topic :: Internet :: WWW/HTTP
5051 Classifier: Topic :: Text Processing :: Markup :: HTML
5152 Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
5253 Classifier: Topic :: Printing
53 Requires-Python: >= 3.5
54 Requires-Python: >=3.5
5455 Description-Content-Type: text/x-rst
5556 Provides-Extra: doc
5657 Provides-Extra: test
00 Metadata-Version: 2.1
11 Name: WeasyPrint
2 Version: 50
2 Version: 51
33 Summary: The Awesome Document Factory
44 Home-page: https://weasyprint.org/
55 Author: Simon Sapin
4646 Classifier: Programming Language :: Python :: 3.5
4747 Classifier: Programming Language :: Python :: 3.6
4848 Classifier: Programming Language :: Python :: 3.7
49 Classifier: Programming Language :: Python :: 3.8
4950 Classifier: Topic :: Internet :: WWW/HTTP
5051 Classifier: Topic :: Text Processing :: Markup :: HTML
5152 Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
5253 Classifier: Topic :: Printing
53 Requires-Python: >= 3.5
54 Requires-Python: >=3.5
5455 Description-Content-Type: text/x-rst
5556 Provides-Extra: doc
5657 Provides-Extra: test
44 tinycss2>=1.0.0
55 cssselect2>=0.1
66 CairoSVG>=2.4.0
7 Pyphen>=0.8
7 Pyphen>=0.9.1
88
99 [doc]
1010 sphinx
2525 Programming Language :: Python :: 3.5
2626 Programming Language :: Python :: 3.6
2727 Programming Language :: Python :: 3.7
28 Programming Language :: Python :: 3.8
2829 Topic :: Internet :: WWW/HTTP
2930 Topic :: Text Processing :: Markup :: HTML
3031 Topic :: Multimedia :: Graphics :: Graphics Conversion
4748 tinycss2>=1.0.0
4849 cssselect2>=0.1
4950 CairoSVG>=2.4.0
50 Pyphen>=0.8
51 Pyphen>=0.9.1
5152 tests_require =
5253 pytest-runner
5354 pytest-cov
0 50
0 51
376376 yield specificity, check_style_attribute(
377377 element, 'font-size:%s' % font_sizes[size])
378378 elif element.tag == 'table':
379 # TODO: we should support cellpadding
380379 if element.get('cellspacing'):
381380 yield specificity, check_style_attribute(
382381 element,
99
1010 """
1111
12 from collections import OrderedDict
1213 from urllib.parse import unquote
1314
1415 from tinycss2.color3 import parse_color
1617 from .. import text
1718 from ..logger import LOGGER
1819 from ..urls import get_link_attribute
19 from .properties import INHERITED, INITIAL_VALUES, Dimension
20 from .properties import (
21 INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, Dimension)
2022 from .utils import (
2123 ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, check_var_function,
2224 safe_urljoin)
2729 # Value in pixels of font-size for <absolute-size> keywords: 12pt (16px) for
2830 # medium, and scaling factors given in CSS3 for others:
2931 # http://www.w3.org/TR/css3-fonts/#font-size-prop
30 # TODO: this will need to be ordered to implement 'smaller' and 'larger'
31 FONT_SIZE_KEYWORDS = dict(
32 FONT_SIZE_KEYWORDS = OrderedDict(
3233 # medium is 16px, others are a ratio of medium
3334 (name, INITIAL_VALUES['font_size'] * a / b)
3435 for name, a, b in (
217218
218219 value = specified[name]
219220 function = getter(name)
221 already_computed_value = False
220222
221223 if value and isinstance(value, tuple) and value[0] == 'var()':
222224 variable_name, default = value[1]
242244 'for property `%s`.', computed_value,
243245 variable_name.replace('_', '-'), name.replace('_', '-'))
244246 if name in INHERITED and parent_style:
247 already_computed_value = True
245248 value = parent_style[name]
246249 else:
250 already_computed_value = name not in INITIAL_NOT_COMPUTED
247251 value = INITIAL_VALUES[name]
248252 else:
249253 value = new_value
250254
251 if function is not None:
255 if function is not None and not already_computed_value:
252256 value = function(computer, name, value)
253257 # else: same as specified
254258
496500 elif value[0] == 'attr()':
497501 assert value[1][1] == 'string'
498502 computed_value = compute_attr_function(computer, value)
499 elif value[0] in ('counter()', 'counters()', 'content()', 'string()'):
503 elif value[0] in (
504 'counter()', 'counters()', 'content()', 'element()',
505 'string()',
506 ):
500507 # Other values need layout context, their computed value cannot be
501508 # better than their specified value yet.
502509 # See build.compute_content_list.
591598 """Compute the ``font-size`` property."""
592599 if value in FONT_SIZE_KEYWORDS:
593600 return FONT_SIZE_KEYWORDS[value]
594 # TODO: support 'larger' and 'smaller'
595
601
602 keyword_values = list(FONT_SIZE_KEYWORDS.values())
596603 parent_font_size = computer['parent_style']['font_size']
597 if value.unit == '%':
604
605 if value == 'larger':
606 for i, keyword_value in enumerate(keyword_values):
607 if keyword_value > parent_font_size:
608 return keyword_values[i]
609 else:
610 return parent_font_size * 1.2
611 elif value == 'smaller':
612 for i, keyword_value in enumerate(keyword_values[::-1]):
613 if keyword_value < parent_font_size:
614 return keyword_values[-i - 1]
615 else:
616 return parent_font_size * 0.8
617 elif value.unit == '%':
598618 return value.value * parent_font_size / 100.
599619 else:
600 return length(computer, name, value, pixels_only=True,
601 font_size=parent_font_size)
620 return length(
621 computer, name, value, pixels_only=True,
622 font_size=parent_font_size)
602623
603624
604625 @register_computer('font-weight')
7575 border-style: hidden;
7676 border-collapse: collapse;
7777 }
78 table[border] { border-style: outset; } /* only if border is not equivalent to zero */
78 table[border]:not([border="0"]) { border-style: outset; }
7979 table[frame=void] { border-style: hidden; }
8080 table[frame=above] { border-style: outset hidden hidden hidden; }
8181 table[frame=below] { border-style: hidden hidden outset hidden; }
8585 table[frame=vsides] { border-style: hidden outset; }
8686 table[frame=box], table[frame=border] { border-style: outset; }
8787
88 table[border] > tr > td, table[border] > tr > th,
89 table[border] > thead > tr > td, table[border] > thead > tr > th,
90 table[border] > tbody > tr > td, table[border] > tbody > tr > th,
91 table[border] > tfoot > tr > td, table[border] > tfoot > tr > th {
92 /* only if border is not equivalent to zero */
88 table[border]:not([border="0"]) > tr > td, table[border]:not([border="0"]) > tr > th,
89 table[border]:not([border="0"]) > thead > tr > td, table[border]:not([border="0"]) > thead > tr > th,
90 table[border]:not([border="0"]) > tbody > tr > td, table[border]:not([border="0"]) > tbody > tr > th,
91 table[border]:not([border="0"]) > tfoot > tr > td, table[border]:not([border="0"]) > tfoot > tr > th {
9392 border-width: 1px;
9493 border-style: inset;
9594 }
480480 return ('content()', ident.lower_value)
481481
482482
483 def check_string_function(token):
483 def check_string_or_element_function(string_or_element, token):
484484 function = parse_function(token)
485485 if function is None:
486486 return
487487 name, args = function
488 if name == 'string' and len(args) in (1, 2):
488 if name == string_or_element and len(args) in (1, 2):
489489 custom_ident = args.pop(0)
490490 if custom_ident.type != 'ident':
491491 return
500500 else:
501501 ident = 'first'
502502
503 return ('string()', (custom_ident, ident))
503 return ('%s()' % string_or_element, (custom_ident, ident))
504504
505505
506506 def check_var_function(token):
530530 elif token.name == 'content':
531531 return check_content_function(token)
532532 elif token.name == 'string':
533 return check_string_function(token)
533 return check_string_or_element_function('string', token)
534534
535535
536536 def get_length(token, negative=True, percentage=False):
742742 elif arg.type == 'string':
743743 string = arg.value
744744 return ('leader()', ('string', string))
745 elif name == 'element':
746 return check_string_or_element_function('element', token)
783783 return length
784784 font_size_keyword = get_keyword(token)
785785 if font_size_keyword in ('smaller', 'larger'):
786 raise InvalidValues('value not supported yet')
786 return font_size_keyword
787787 if font_size_keyword in computed_values.FONT_SIZE_KEYWORDS:
788 # or keyword in ('smaller', 'larger')
789788 return font_size_keyword
790789
791790
967966
968967
969968 @property()
970 @single_keyword
971 def position(keyword):
969 @single_token
970 def position(token):
972971 """``position`` property validation."""
973 return keyword in ('static', 'relative', 'absolute', 'fixed')
972 if token.type == 'function' and token.name == 'running':
973 if len(token.arguments) == 1 and token.arguments[0].type == 'ident':
974 return ('running()', token.arguments[0].value)
975 keyword = get_single_keyword([token])
976 if keyword in ('static', 'relative', 'absolute', 'fixed'):
977 return keyword
974978
975979
976980 @property()
13641368 if None not in parsed_tokens:
13651369 return (var_name, parsed_tokens)
13661370 elif tokens and get_keyword(tokens[0]) == 'none':
1367 return 'none'
1371 return 'none', ()
13681372
13691373
13701374 @property()
13711375 def transform(tokens):
1376 """Validation for ``transform``."""
13721377 if get_single_keyword(tokens) == 'none':
13731378 return ()
13741379 else:
13761381 for token in tokens:
13771382 function = parse_function(token)
13781383 if not function:
1379 raise InvalidValues
1384 return
13801385 name, args = function
13811386
13821387 if len(args) == 1:
13971402 elif name == 'scale' and args[0].type == 'number':
13981403 transforms.append(('scale', (args[0].value,) * 2))
13991404 else:
1400 raise InvalidValues
1405 return
14011406 elif len(args) == 2:
14021407 if name == 'scale' and all(a.type == 'number' for a in args):
14031408 transforms.append((name, tuple(arg.value for arg in args)))
14071412 if name == 'translate' and all(lengths):
14081413 transforms.append((name, lengths))
14091414 else:
1410 raise InvalidValues
1415 return
14111416 elif len(args) == 6 and name == 'matrix' and all(
14121417 a.type == 'number' for a in args):
14131418 transforms.append((name, tuple(arg.value for arg in args)))
14141419 else:
1415 raise InvalidValues
1420 return
14161421 return tuple(transforms)
8888 def all_children(self):
8989 return ()
9090
91 def __init__(self, element_tag, style):
91 def __init__(self, element_tag, style, element):
9292 self.element_tag = element_tag
93 self.element = element
9394 self.style = style
9495
9596 def __repr__(self):
100101 """Return an anonymous box that inherits from ``parent``."""
101102 style = computed_from_cascaded(
102103 cascaded={}, parent_style=parent.style, element=None)
103 return cls(parent.element_tag, style, *args, **kwargs)
104 return cls(parent.element_tag, style, parent.element, *args, **kwargs)
104105
105106 def copy(self):
106107 """Return shallow copy of the box."""
111112 # Copy attributes
112113 new_box.__dict__.update(self.__dict__)
113114 return new_box
115
116 def deepcopy(self):
117 """Return a copy of the box with recursive copies of its children."""
118 return self.copy()
114119
115120 def translate(self, dx=0, dy=0, ignore_floats=False):
116121 """Change the box’s position.
273278 """Return whether this box is in the absolute positioning scheme."""
274279 return self.style['position'] in ('absolute', 'fixed')
275280
281 def is_running(self):
282 """Return whether this box is a running element."""
283 return self.style['position'][0] == 'running()'
284
276285 def is_in_normal_flow(self):
277286 """Return whether this box is in normal flow."""
278 return not (self.is_floated() or self.is_absolutely_positioned())
287 return not (
288 self.is_floated() or self.is_absolutely_positioned() or
289 self.is_running())
279290
280291 # Start and end page values for named pages
281292
286297
287298 class ParentBox(Box):
288299 """A box that has children."""
289 def __init__(self, element_tag, style, children):
290 super(ParentBox, self).__init__(element_tag, style)
300 def __init__(self, element_tag, style, element, children):
301 super().__init__(element_tag, style, element)
291302 self.children = tuple(children)
292303
293304 def all_children(self):
332343 new_box._remove_decoration(not is_start, not is_end)
333344 return new_box
334345
346 def deepcopy(self):
347 result = self.copy()
348 result.children = tuple(child.deepcopy() for child in self.children)
349 return result
350
335351 def descendants(self):
336352 """A flat generator for a box, its children and descendants."""
337353 yield self
352368 raise ValueError('Table wrapper without a table')
353369
354370 def page_values(self):
355 start_value, end_value = super(ParentBox, self).page_values()
371 start_value, end_value = super().page_values()
356372 if self.children:
357 start_box, end_box = self.children[0], self.children[-1]
358 start_value = start_box.page_values()[0] or start_value
359 end_value = end_box.page_values()[1] or end_value
373 if len(self.children) == 1:
374 page_values = self.children[0].page_values()
375 start_value = page_values[0] or start_value
376 end_value = page_values[1] or end_value
377 else:
378 start_box, end_box = self.children[0], self.children[-1]
379 start_value = start_box.page_values()[0] or start_value
380 end_value = end_box.page_values()[1] or end_value
360381 return start_value, end_value
361382
362383
466487 ascii_to_wide = dict((i, chr(i + 0xfee0)) for i in range(0x21, 0x7f))
467488 ascii_to_wide.update({0x20: '\u3000', 0x2D: '\u2212'})
468489
469 def __init__(self, element_tag, style, text):
490 def __init__(self, element_tag, style, element, text):
470491 assert text
471 super(TextBox, self).__init__(element_tag, style)
492 super().__init__(element_tag, style, element)
472493 text_transform = style['text_transform']
473494 if text_transform != 'none':
474495 text = {
516537 and is opaque from CSS’s point of view.
517538
518539 """
519 def __init__(self, element_tag, style, replacement):
520 super(ReplacedBox, self).__init__(element_tag, style)
540 def __init__(self, element_tag, style, element, replacement):
541 super().__init__(element_tag, style, element)
521542 self.replacement = replacement
522543
523544
554575 return
555576 self.column_positions = [
556577 position + dx for position in self.column_positions]
557 return super(TableBox, self).translate(dx, dy, ignore_floats)
578 return super().translate(dx, dy, ignore_floats)
558579
559580 def page_values(self):
560581 return (self.style['page'], self.style['page'])
666687 def __init__(self, page_type, style):
667688 self.page_type = page_type
668689 # Page boxes are not linked to any element.
669 super(PageBox, self).__init__(
670 element_tag=None, style=style, children=[])
690 super().__init__(
691 element_tag=None, style=style, element=None, children=[])
671692
672693 def __repr__(self):
673694 return '<%s %s>' % (type(self).__name__, self.page_type)
678699 def __init__(self, at_keyword, style):
679700 self.at_keyword = at_keyword
680701 # Margin boxes are not linked to any element.
681 super(MarginBox, self).__init__(
682 element_tag=None, style=style, children=[])
702 super().__init__(
703 element_tag=None, style=style, element=None, children=[])
683704
684705 def __repr__(self):
685706 return '<%s %s>' % (type(self).__name__, self.at_keyword)
7979 return box
8080
8181
82 def make_box(element_tag, style, content):
83 return BOX_TYPE_FROM_DISPLAY[style['display']](
84 element_tag, style, content)
82 def make_box(element_tag, style, content, element):
83 box = BOX_TYPE_FROM_DISPLAY[style['display']](
84 element_tag, style, element, content)
85 return box
8586
8687
8788 def element_to_box(element, style_for, get_image_from_uri, base_url,
121122 if display == 'none':
122123 return []
123124
124 box = make_box(element.tag, style, [])
125 box = make_box(element.tag, style, [], element)
125126
126127 if state is None:
127128 # use a list to have a shared mutable object
226227 if 'none' in (display, content) or content in ('normal', 'inhibit'):
227228 return []
228229
229 box = make_box('%s::%s' % (element.tag, pseudo_type), style, [])
230 box = make_box('%s::%s' % (element.tag, pseudo_type), style, [], element)
230231
231232 quote_depth, counter_values, _counter_scopes = state
232233 update_counters(state, style)
263264 # `content` where 'normal' computes as 'inhibit' for pseudo elements.
264265 quote_depth, counter_values, _counter_scopes = state
265266
266 box = make_box('%s::marker' % element.tag, style, children)
267 box = make_box('%s::marker' % element.tag, style, children, element)
267268
268269 if style['display'] == 'none':
269270 return
408409 counters.format(counter_value, counter_style)
409410 for counter_value in counter_values.get(counter_name, [0])))
410411 elif type_ == 'string()':
411 if in_page_context:
412 texts.append(context.get_string_set_for(page, *value))
413 else:
412 if not in_page_context:
414413 # string() is currently only valid in @page context
415414 # See https://github.com/Kozea/WeasyPrint/issues/723
416415 LOGGER.warning(
417416 '"string(%s)" is only allowed in page margins' %
418417 (' '.join(value)))
418 continue
419 texts.append(context.get_string_set_for(page, *value) or '')
419420 elif type_ == 'target-counter()':
420421 anchor_token, counter_name, counter_style = value
421422 lookup_target = target_collector.lookup_target(
491492 texts.append(quotes[min(quote_depth[0], len(quotes) - 1)])
492493 if is_open:
493494 quote_depth[0] += 1
495 elif type_ == 'element()':
496 if not in_page_context:
497 LOGGER.warning(
498 '"element(%s)" is only allowed in page margins' %
499 (' '.join(value)))
500 continue
501 new_box = context.get_running_element_for(page, *value)
502 if new_box is None:
503 continue
504 new_box = new_box.deepcopy()
505 new_box.style['position'] = 'static'
506 for child in new_box.descendants():
507 if child.style['content'] in ('normal', 'none'):
508 continue
509 child.children = content_to_boxes(
510 child.style, child, quote_depth, counter_values,
511 get_image_from_uri, target_collector, context=context,
512 page=page)
513 boxlist.append(new_box)
494514 text = ''.join(texts)
495515 if text:
496516 boxlist.append(boxes.TextBox.anonymous_from(parent_box, text))
718738 See http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
719739
720740 """
721 if not isinstance(box, boxes.ParentBox):
741 if not isinstance(box, boxes.ParentBox) or box.is_running():
722742 return box
723743
724744 # Do recursion.
11111131 See http://www.w3.org/TR/css-flexbox-1/#flex-items
11121132
11131133 """
1114 if not isinstance(box, boxes.ParentBox):
1134 if not isinstance(box, boxes.ParentBox) or box.is_running():
11151135 return box
11161136
11171137 # Do recursion.
11951215 box.text = text
11961216 return following_collapsible_space
11971217
1198 if isinstance(box, boxes.ParentBox):
1218 if isinstance(box, boxes.ParentBox) and not box.is_running():
11991219 for child in box.children:
12001220 if isinstance(child, (boxes.TextBox, boxes.InlineBox)):
12011221 following_collapsible_space = process_whitespace(
12461266 ]
12471267
12481268 """
1249 if not isinstance(box, boxes.ParentBox):
1269 if not isinstance(box, boxes.ParentBox) or box.is_running():
12501270 return box
12511271
12521272 box_children = list(box.children)
13811401 ]
13821402
13831403 """
1384 if not isinstance(box, boxes.ParentBox):
1404 if not isinstance(box, boxes.ParentBox) or box.is_running():
13851405 return box
13861406
13871407 new_children = []
110110 else:
111111 # TODO: support images with 'display: table-cell'?
112112 type_ = boxes.InlineReplacedBox
113 new_box = type_(element.tag, box.style, image)
113 new_box = type_(element.tag, box.style, element, image)
114114 # TODO: check other attributes that need to be copied
115115 # TODO: find another solution
116116 new_box.string_set = box.string_set
9494 """
9595 @property
9696 def device_units_per_user_units(self):
97 scale = super(ScaledSVGSurface, self).device_units_per_user_units
97 scale = super().device_units_per_user_units
9898 return scale / 0.75
9999
100100
195195 self._excluded_shapes_lists = []
196196 self.excluded_shapes = None # Not initialized yet
197197 self.string_set = defaultdict(lambda: defaultdict(lambda: list()))
198 self.running_elements = defaultdict(
199 lambda: defaultdict(lambda: list()))
198200 self.current_page = None
199201 self.forced_break = False
200202
223225 self.excluded_shapes = None
224226
225227 def get_string_set_for(self, page, name, keyword='first'):
226 """Resolve value of string function (as set by string set).
228 """Resolve value of string function."""
229 return self.get_string_or_element_for(
230 self.string_set, page, name, keyword)
231
232 def get_running_element_for(self, page, name, keyword='first'):
233 """Resolve value of element function."""
234 return self.get_string_or_element_for(
235 self.running_elements, page, name, keyword)
236
237 def get_string_or_element_for(self, store, page, name, keyword):
238 """Resolve value of string or element function.
227239
228240 We'll have something like this that represents all assignments on a
229241 given page:
230242
231 {1: [u'First Header'], 3: [u'Second Header'],
232 4: [u'Third Header', u'3.5th Header']}
243 {1: ['First Header'], 3: ['Second Header'],
244 4: ['Third Header', '3.5th Header']}
233245
234246 Value depends on current page.
235247 http://dev.w3.org/csswg/css-gcpm/#funcdef-string
236248
237 :param name: the name of the named string.
238 :param keyword: indicates which value of the named string to use.
239 Default is the first assignment on the current page
240 else the most recent assignment (entry value)
241 :returns: text
249 :param store: dictionary where the resolved value is stored.
250 :param page: current page.
251 :param name: name of the named string or running element.
252 :param keyword: indicates which value of the named string or running
253 element to use. Default is the first assignment on the
254 current page else the most recent assignment.
255 :returns: text for string set, box for running element
242256
243257 """
244 if self.current_page in self.string_set[name]:
258 if self.current_page in store[name]:
245259 # A value was assigned on this page
246 first_string = self.string_set[name][self.current_page][0]
247 last_string = self.string_set[name][self.current_page][-1]
260 first_string = store[name][self.current_page][0]
261 last_string = store[name][self.current_page][-1]
248262 if keyword == 'first':
249263 return first_string
250264 elif keyword == 'start':
261275 break
262276 elif keyword == 'last':
263277 return last_string
278 elif keyword == 'first-except':
279 return
264280 # Search backwards through previous pages
265281 for previous_page in range(self.current_page - 1, 0, -1):
266 if previous_page in self.string_set[name]:
267 return self.string_set[name][previous_page][-1]
268 return ''
282 if previous_page in store[name]:
283 return store[name][previous_page][-1]
257257 # TODO: boxes.FlexBox is allowed here because flex_layout calls
258258 # block_container_layout, there's probably a better solution.
259259 assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox))
260
261 # TODO: this should make a difference, but that is currently neglected.
262 # See http://www.w3.org/TR/CSS21/visudet.html#normal-block
263 # http://www.w3.org/TR/CSS21/visudet.html#root-height
264
265 # if box.style['overflow'] != 'visible':
266 # ...
267260
268261 # We have to work around floating point rounding errors here.
269262 # The 1e-9 value comes from PEP 485.
358351 break
359352 resume_at = (index, None)
360353 break
354 elif child.is_running():
355 running_name = child.style['position'][1]
356 page = context.current_page
357 context.running_elements[running_name][page].append(child)
361358 continue
362359
363360 if isinstance(child, boxes.LineBox):
277277 if child.element_tag.endswith('::first-letter'):
278278 letter_box = boxes.InlineBox(
279279 '%s::first-letter' % box.element_tag, letter_style,
280 [child])
280 box.element, [child])
281281 box.children = (
282282 (letter_box,) + tuple(box.children[1:]))
283283 elif child.text:
304304 if first_letter_style['float'] == 'none':
305305 letter_box = boxes.InlineBox(
306306 '%s::first-letter' % box.element_tag,
307 first_letter_style, [])
307 first_letter_style, box.element, [])
308308 text_box = boxes.TextBox(
309309 '%s::first-letter' % box.element_tag, letter_style,
310 first_letter)
310 box.element, first_letter)
311311 letter_box.children = (text_box,)
312312 box.children = (letter_box,) + tuple(box.children)
313313 else:
314314 letter_box = boxes.BlockBox(
315315 '%s::first-letter' % box.element_tag,
316 first_letter_style, [])
316 first_letter_style, box.element, [])
317317 letter_box.first_letter_style = None
318318 line_box = boxes.LineBox(
319319 '%s::first-letter' % box.element_tag, letter_style,
320 [])
320 box.element, [])
321321 letter_box.children = (line_box,)
322322 text_box = boxes.TextBox(
323323 '%s::first-letter' % box.element_tag, letter_style,
324 first_letter)
324 box.element, first_letter)
325325 line_box.children = (text_box,)
326326 box.children = (letter_box,) + tuple(box.children)
327327 if skip_stack and child_skip_stack:
737737 old_child.translate(dx=dx)
738738 float_resume_at = index + 1
739739 continue
740 elif child.is_running():
741 running_name = child.style['position'][1]
742 page = context.current_page
743 context.running_elements[running_name][page].append(child)
744 continue
740745
741746 last_child = (index == len(box.children) - 1)
742747 available_width = max_x
857862 # add the original skip stack to the partial
858863 # skip stack we get after the new rendering.
859864
860 # We have to do:
861 # resume_at + initial_skip_stack
862 # but adding skip stacks is a bit complicated
863 current_skip_stack = initial_skip_stack
864 current_resume_at = (child_index, child_resume_at)
865 # Combining skip stacks is a bit complicated
866 # We have to:
867 # - set `child_index` as the first number
868 # - append the new stack if it's an absolute one
869 # - otherwise append the combined stacks
870 # (resume_at + initial_skip_stack)
871
872 # extract the initial index
873 if initial_skip_stack is None:
874 current_skip_stack = None
875 initial_index = 0
876 else:
877 initial_index, current_skip_stack = (
878 initial_skip_stack)
879 # child_resume_at is an absolute skip stack
880 if child_index > initial_index:
881 resume_at = (child_index, child_resume_at)
882 break
883
884 # combine the stacks
885 current_resume_at = child_resume_at
865886 stack = []
866887 while current_skip_stack and current_resume_at:
867888 skip, current_skip_stack = (
874895 resume_at = current_resume_at
875896 while stack:
876897 resume_at = (stack.pop(), resume_at)
898 # insert the child index
899 resume_at = (child_index, resume_at)
877900 break
878901 if break_found:
879902 break
344344 box.style, box, quote_depth, counter_values,
345345 context.get_image_from_uri, context.target_collector, context,
346346 page)
347 # content_to_boxes() only produces inline-level boxes, no need to
348 # run other post-processors from build.build_formatting_structure()
347 build.process_whitespace(box)
348 box = build.anonymous_table_boxes(box)
349 box = build.flex_boxes(box)
349350 box = build.inline_in_block(box)
350 build.process_whitespace(box)
351 box = build.block_in_inline(box)
351352 resolve_percentages(box, containing_block)
352353 if not box.is_generated:
353354 box.width = box.height = 0
181181 else:
182182 row.height = max(row.height, max(
183183 row_cell.height for row_cell in ending_cells))
184 row_bottom_y = cell.position_y + row.height
184 row_bottom_y = row.position_y + row.height
185185 else:
186186 row_bottom_y = row.position_y
187187 row.height = 0
326326 # Layout for row groups, rows and cells
327327 position_y = table.content_box_y() + border_spacing_y
328328 initial_position_y = position_y
329 table_rows = [
330 child for child in table.children
331 if not child.is_header and not child.is_footer]
329332
330333 def all_groups_layout():
334 # If the page is not empty, we try to render the header and the footer
335 # on it. If the table does not fit on the page, we try to render it on
336 # the next page.
337
338 # If the page is empty and the header and footer are too big, there
339 # are not rendered. If no row can be rendered because of the header and
340 # the footer, the header and/or the footer are not rendered.
341
342 if page_is_empty:
343 header_footer_max_position_y = max_position_y
344 else:
345 header_footer_max_position_y = float('inf')
346
331347 if table.children and table.children[0].is_header:
332348 header = table.children[0]
333349 header, resume_at, next_page = group_layout(
334 header, position_y, max_position_y,
350 header, position_y, header_footer_max_position_y,
335351 skip_stack=None, page_is_empty=False)
336352 if header and not resume_at:
337353 header_height = header.height + border_spacing_y
343359 if table.children and table.children[-1].is_footer:
344360 footer = table.children[-1]
345361 footer, resume_at, next_page = group_layout(
346 footer, position_y, max_position_y,
362 footer, position_y, header_footer_max_position_y,
347363 skip_stack=None, page_is_empty=False)
348364 if footer and not resume_at:
349365 footer_height = footer.height + border_spacing_y
369385 position_y=position_y + header_height,
370386 max_position_y=max_position_y - footer_height,
371387 page_is_empty=avoid_breaks))
372 if new_table_children or not page_is_empty:
388 if new_table_children or not table_rows or not page_is_empty:
373389 footer.translate(dy=end_position_y - footer.position_y)
374390 end_position_y += footer_height
375391 return (header, new_table_children, footer,
386402 position_y=position_y + header_height,
387403 max_position_y=max_position_y,
388404 page_is_empty=avoid_breaks))
389 if new_table_children or not page_is_empty:
405 if new_table_children or not table_rows or not page_is_empty:
390406 return (header, new_table_children, footer,
391407 end_position_y, resume_at, next_page)
392408 else:
401417 position_y=position_y,
402418 max_position_y=max_position_y - footer_height,
403419 page_is_empty=avoid_breaks))
404 if new_table_children or not page_is_empty:
420 if new_table_children or not table_rows or not page_is_empty:
405421 footer.translate(dy=end_position_y - footer.position_y)
406422 end_position_y += footer_height
407423 return (header, new_table_children, footer,
7171 return '({0})'.format(pdf_escape(
7272 ('\ufeff' + value).encode('utf-16-be').decode('latin1')))
7373 else:
74 return super(PDFFormatter, self).convert_field(value, conversion)
74 return super().convert_field(value, conversion)
7575
7676 def vformat(self, format_string, args, kwargs):
77 result = super(PDFFormatter, self).vformat(format_string, args, kwargs)
77 result = super().vformat(format_string, args, kwargs)
7878 return result.encode('latin1')
7979
8080
77 :license: BSD, see LICENSE for details.
88
99 """
10
11 from math import isclose
1012
1113 import pytest
1214 import tinycss2
8284 style_for = get_all_computed_styles(
8385 document, user_stylesheets=[CSS(resource_filename('user.css'))])
8486
85 # Element objects behave a lists of their children
87 # Element objects behave as lists of their children
8688 _head, body = document.etree_element
8789 h1, p, ul, div = body
8890 li_0, _li_1 = ul
450452 body, = html.children
451453 p, = body.children
452454 assert p.margin_left == width
455
456
457 @assert_no_logs
458 @pytest.mark.parametrize('parent_css, parent_size, child_css, child_size', (
459 ('10px', 10, '10px', 10),
460 ('x-small', 12, 'xx-large', 32),
461 ('x-large', 24, '2em', 48),
462 ('1em', 16, '1em', 16),
463 ('1em', 16, 'larger', 6 / 5 * 16),
464 ('medium', 16, 'larger', 6 / 5 * 16),
465 ('x-large', 24, 'larger', 32),
466 ('xx-large', 32, 'larger', 1.2 * 32),
467 ('1px', 1, 'larger', 3 / 5 * 16),
468 ('28px', 28, 'larger', 32),
469 ('100px', 100, 'larger', 120),
470 ('xx-small', 3 / 5 * 16, 'larger', 12),
471 ('1em', 16, 'smaller', 8 / 9 * 16),
472 ('medium', 16, 'smaller', 8 / 9 * 16),
473 ('x-large', 24, 'smaller', 6 / 5 * 16),
474 ('xx-large', 32, 'smaller', 24),
475 ('xx-small', 3 / 5 * 16, 'smaller', 0.8 * 3 / 5 * 16),
476 ('1px', 1, 'smaller', 0.8),
477 ('28px', 28, 'smaller', 24),
478 ('100px', 100, 'smaller', 32),
479 ))
480 def test_font_size(parent_css, parent_size, child_css, child_size):
481 document = FakeHTML(string='<p>a<span>b')
482 style_for = get_all_computed_styles(document, user_stylesheets=[CSS(
483 string='p{font-size:%s}span{font-size:%s}' % (parent_css, child_css))])
484
485 _head, body = document.etree_element
486 p, = body
487 span, = p
488 assert isclose(style_for(p)['font_size'], parent_size)
489 assert isclose(style_for(span)['font_size'], child_size)
379379 assert line2.children[0].children[0].children[0].text == 'yyyyyy yyy'
380380 assert line3.children[0].children[0].text == 'ZZZZZZ zzzzz'
381381 assert line4.children[0].text == ')x'
382
383
384 @assert_no_logs
385 def test_breaking_linebox_regression_11():
386 # Regression test for https://github.com/Kozea/WeasyPrint/issues/953
387 page, = parse(
388 '<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
389 '<p style="width:10em; font-family: ahem">'
390 ' line 1<br><span>123 567 90</span>x'
391 '</p>')
392 html, = page.children
393 body, = html.children
394 p, = body.children
395 line1, line2, line3 = p.children
396 assert line1.children[0].text == 'line 1'
397 assert line2.children[0].children[0].text == '123 567'
398 assert line3.children[0].children[0].text == '90'
399 assert line3.children[1].text == 'x'
400
401
402 @assert_no_logs
403 def test_breaking_linebox_regression_12():
404 # Regression test for https://github.com/Kozea/WeasyPrint/issues/953
405 page, = parse(
406 '<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
407 '<p style="width:10em; font-family: ahem">'
408 ' <br><span>123 567 90</span>x'
409 '</p>')
410 html, = page.children
411 body, = html.children
412 p, = body.children
413 line1, line2, line3 = p.children
414 assert line2.children[0].children[0].text == '123 567'
415 assert line3.children[0].children[0].text == '90'
416 assert line3.children[1].text == 'x'
417
418
419 @assert_no_logs
420 def test_breaking_linebox_regression_13():
421 # Regression test for https://github.com/Kozea/WeasyPrint/issues/953
422 page, = parse(
423 '<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
424 '<p style="width:10em; font-family: ahem">'
425 ' 123 567 90 <span>123 567 90</span>x'
426 '</p>')
427 html, = page.children
428 body, = html.children
429 p, = body.children
430 line1, line2, line3 = p.children
431 assert line1.children[0].text == '123 567 90'
432 assert line2.children[0].children[0].text == '123 567'
433 assert line3.children[0].children[0].text == '90'
434 assert line3.children[1].text == 'x'
382435
383436
384437 @assert_no_logs
12131213 assert line_1.position_y == 3
12141214 assert line_2.position_y == 43
12151215 assert line_3.position_y == 83
1216
1217
1218 @assert_no_logs
1219 def test_margin_boxes_element():
1220 pages = render_pages('''
1221 <style>
1222 footer {
1223 position: running(footer);
1224 }
1225 @page {
1226 margin: 50px;
1227 size: 200px;
1228 @bottom-center {
1229 content: element(footer);
1230 }
1231 }
1232 h1 {
1233 height: 40px;
1234 }
1235 .pages:before {
1236 content: counter(page);
1237 }
1238 .pages:after {
1239 content: counter(pages);
1240 }
1241 </style>
1242 <footer class="pages"> of </footer>
1243 <h1>test1</h1>
1244 <h1>test2</h1>
1245 <h1>test3</h1>
1246 <h1>test4</h1>
1247 <h1>test5</h1>
1248 <h1>test6</h1>
1249 <footer>Static</footer>
1250 ''')
1251 footer1_text = ''.join(
1252 getattr(node, 'text', '')
1253 for node in pages[0].children[1].descendants())
1254 assert footer1_text == '1 of 3'
1255
1256 footer2_text = ''.join(
1257 getattr(node, 'text', '')
1258 for node in pages[1].children[1].descendants())
1259 assert footer2_text == '2 of 3'
1260
1261 footer3_text = ''.join(
1262 getattr(node, 'text', '')
1263 for node in pages[2].children[1].descendants())
1264 assert footer3_text == 'Static'
1265
1266
1267 @assert_no_logs
1268 @pytest.mark.parametrize('argument, texts', (
1269 # TODO: start doesn’t work because running elements are removed from the
1270 # original tree, and the current implentation in
1271 # layout.get_running_element_for uses the tree to know if it’s at the
1272 # beginning of the page
1273
1274 # ('start', ('', '2-first', '2-last', '3-last', '5')),
1275
1276 ('first', ('', '2-first', '3-first', '3-last', '5')),
1277 ('last', ('', '2-last', '3-last', '3-last', '5')),
1278 ('first-except', ('', '', '', '3-last', '')),
1279 ))
1280 def test_running_elements(argument, texts):
1281 pages = render_pages('''
1282 <style>
1283 @page {
1284 margin: 50px;
1285 size: 200px;
1286 @bottom-center { content: element(title %s) }
1287 }
1288 article { break-after: page }
1289 h1 { position: running(title) }
1290 </style>
1291 <article>
1292 <div>1</div>
1293 </article>
1294 <article>
1295 <h1>2-first</h1>
1296 <h1>2-last</h1>
1297 </article>
1298 <article>
1299 <p>3</p>
1300 <h1>3-first</h1>
1301 <h1>3-last</h1>
1302 </article>
1303 <article>
1304 </article>
1305 <article>
1306 <h1>5</h1>
1307 </article>
1308 ''' % argument)
1309 assert len(pages) == 5
1310 for page, text in zip(pages, texts):
1311 html, margin = page.children
1312 if margin.children:
1313 h1, = margin.children
1314 line, = h1.children
1315 textbox, = line.children
1316 assert textbox.text == text
1317 else:
1318 assert not text
1319
1320
1321 @assert_no_logs
1322 def test_running_elements_display():
1323 page, = render_pages('''
1324 <style>
1325 @page {
1326 margin: 50px;
1327 size: 200px;
1328 @bottom-left { content: element(inline) }
1329 @bottom-center { content: element(block) }
1330 @bottom-right { content: element(table) }
1331 }
1332 table { position: running(table) }
1333 div { position: running(block) }
1334 span { position: running(inline) }
1335 </style>
1336 text
1337 <table><tr><td>table</td></tr></table>
1338 <div>block</div>
1339 <span>inline</span>
1340 ''')
1341 html, left, center, right = page.children
1342 assert ''.join(
1343 getattr(node, 'text', '') for node in left.descendants()) == 'inline'
1344 assert ''.join(
1345 getattr(node, 'text', '') for node in center.descendants()) == 'block'
1346 assert ''.join(
1347 getattr(node, 'text', '') for node in right.descendants()) == 'table'
25692569 assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 50)
25702570 assert (caption.content_box_x(), caption.content_box_y()) == (40, 80)
25712571 assert (h2.content_box_x(), h2.content_box_y()) == (20, 130)
2572
2573
2574 @assert_no_logs
2575 @pytest.mark.parametrize('rows_expected, thead, tfoot, content', (
2576 ([[], ['Header', 'Footer']], 45, 45, '<p>content</p>'),
2577 ([[], ['Header', 'Footer']], 85, 5, '<p>content</p>'),
2578 ([['Header', 'Footer']], 30, 30, '<p>content</p>'),
2579 ([[], ['Header']], 30, 110, '<p>content</p>'),
2580 ([[], ['Header', 'Footer']], 30, 60, '<p>content</p>'),
2581 ([[], ['Footer']], 110, 30, '<p>content</p>'),
2582
2583 # We try to render the header and footer on the same page, but it does not
2584 # fit. So we try to render the header or the footer on the next one, but
2585 # nothing fit either.
2586 ([[], []], 110, 110, '<p>content</p>'),
2587
2588 ([['Header', 'Footer']], 30, 30, ''),
2589 ([['Header']], 30, 110, ''),
2590 ([['Header', 'Footer']], 30, 60, ''),
2591 ([['Footer']], 110, 30, ''),
2592 ([[]], 110, 110, ''),
2593 ))
2594 def test_table_empty_body(rows_expected, thead, tfoot, content):
2595 html = '''
2596 <style>
2597 @page { size: 100px }
2598 p { height: 20px }
2599 thead th { height: %spx }
2600 tfoot th { height: %spx }
2601 </style>
2602 %s
2603 <table>
2604 <thead><tr><th>Header</th></tr></thead>
2605 <tfoot><tr><th>Footer</th></tr></tfoot>
2606 </table>
2607 ''' % (thead, tfoot, content)
2608 pages = render_pages(html)
2609 assert len(pages) == len(rows_expected)
2610 for i, page in enumerate(pages):
2611 rows = []
2612 html, = page.children
2613 body, = html.children
2614 table_wrapper = body.children[-1]
2615 if not table_wrapper.is_table_wrapper:
2616 assert rows == rows_expected[i]
2617 continue
2618 table, = table_wrapper.children
2619 for group in table.children:
2620 for row in group.children:
2621 cell, = row.children
2622 line, = cell.children
2623 text, = line.children
2624 rows.append(text.text)
2625 assert rows == rows_expected[i]
88
99 """
1010
11 import pytest
12
13 from ..css.properties import KNOWN_PROPERTIES
1114 from .test_boxes import render_pages as parse
1215
1316
9497 body, = html.children
9598 paragraph, = body.children
9699 assert paragraph.width == 10
100
101
102 @pytest.mark.parametrize('prop', KNOWN_PROPERTIES)
103 def test_variable_fallback(prop):
104 parse('''
105 <style>
106 div {
107 --var: improperValue;
108 %s: var(--var);
109 }
110 </style>
111 <div></div>
112 ''' % prop)
146146 void g_type_init (void);
147147
148148 void pango_layout_set_width (PangoLayout *layout, int width);
149 PangoAttrList * pango_layout_get_attributes(PangoLayout *layout);
149150 void pango_layout_set_attributes (
150151 PangoLayout *layout, PangoAttrList *attrs);
151152 void pango_layout_set_text (
765766 if text and (word_spacing != 0 or letter_spacing != 0):
766767 letter_spacing = units_from_double(letter_spacing)
767768 space_spacing = units_from_double(word_spacing) + letter_spacing
768 attr_list = pango.pango_attr_list_new()
769 attr_list = pango.pango_layout_get_attributes(self.layout)
770 if not attr_list:
771 # TODO: list should be freed
772 attr_list = pango.pango_attr_list_new()
769773
770774 def add_attr(start, end, spacing):
771775 # TODO: attributes should be freed
780784 position = bytestring.find(b' ', position + 1)
781785
782786 pango.pango_layout_set_attributes(self.layout, attr_list)
783 pango.pango_attr_list_unref(attr_list)
784787
785788 # Tabs width
786789 if b'\t' in bytestring: