1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 00:21:15 +03:00

Merge branch 'master' into fix_943

This commit is contained in:
Guillaume Ayoub 2019-12-15 15:26:00 +01:00
commit 7eacb711a7
19 changed files with 248 additions and 56 deletions

View File

@ -11,15 +11,19 @@ matrix:
python: 3.5
- os: linux
python: 3.6
- dist: xenial
- os: linux
python: 3.7
- dist: bionic
python: 3.8
- os: osx
language: generic
env: PYTHON=python3
env:
- HOMEBREW_NO_AUTO_UPDATE=1
- PYTHON=python3
- os: windows
# Windows doesn't support python or even generic language
language: cpp
env: PYTHON=/c/Python37/python
env: PYTHON=/c/Python38/python
allow_failures:
- os: windows
@ -41,10 +45,7 @@ before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then mkdir -p ~/.fonts; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then wget "https://github.com/Kozea/Ahem/blob/master/Ahem.ttf?raw=true" -O ~/.fonts/Ahem.ttf; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew tap caskroom/fonts; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew pin numpy gdal postgis; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew upgrade python; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew tap homebrew/cask-fonts; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew cask install font-dejavu-sans; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install cairo pango gdk-pixbuf libffi; fi

View File

@ -303,17 +303,18 @@ The `CSS Generated Content for Paged Media Module`_ (GCPM) is a working draft
defining "new properties and values, so that authors may bring new techniques
(running headers and footers, footnotes, page selection) to paged media".
`Page selectors`_ are also supported by WeasyPrint. You can select pages
according to their position in the document:
`Page selectors`_ are supported by WeasyPrint. You can select pages according
to their position in the document:
.. code-block:: css
@page :nth(3) { background: red } /* Third page */
@page :nth(2n+1) { background: green } /* Odd pages */
You can also use `running elements`_ to put HTML boxes into the page margins.
The other features of GCPM are **not** implemented:
- running elements (``running()`` and ``element()``);
- footnotes (``float: footnote``, ``footnote-display``, ``footnote`` counter,
``::footnote-call``, ``::footnote-marker``, ``@footnote`` rule,
``footnote-policy``);
@ -321,6 +322,7 @@ The other features of GCPM are **not** implemented:
.. _CSS Generated Content for Paged Media Module: http://www.w3.org/TR/css-gcpm-3/
.. _Page selectors: https://www.w3.org/TR/css-gcpm-3/#document-page-selectors
.. _running elements: https://www.w3.org/TR/css-gcpm-3/#running-elements
CSS Generated Content Module Level 3

View File

@ -13,7 +13,7 @@ WeasyPrint |version| depends on:
* tinycss2_ ≥ 1.0.0
* cssselect2_ ≥ 0.1
* CairoSVG_ ≥ 2.4.0
* Pyphen_ ≥ 0.8
* Pyphen_ ≥ 0.9.1
* GDK-PixBuf_ ≥ 2.25.0 [#]_
.. _CPython: http://www.python.org/
@ -113,12 +113,19 @@ If your favorite system is not listed here but you know the package names,
Debian / Ubuntu
~~~~~~~~~~~~~~~
Debian 9.0 Stretch or newer, Ubuntu 16.04 Xenial or newer:
WeasyPrint is `packaged for Debian 11 or newer
<https://packages.debian.org/search?searchon=names&keywords=weasyprint>`_.
You can install it with pip on Debian 10 Buster or newer, or on Ubuntu 18.04
Bionic Beaver or newer, after installing the following packages:
.. code-block:: sh
sudo apt-get install build-essential python3-dev python3-pip python3-setuptools python3-wheel python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info
WeasyPrint may work under previous releases of Debian or Ubuntu, but they often
provide an old version of Cairo that may limit WeasyPrint's features [1]_.
Fedora
~~~~~~

View File

@ -26,6 +26,7 @@ classifiers =
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Topic :: Internet :: WWW/HTTP
Topic :: Text Processing :: Markup :: HTML
Topic :: Multimedia :: Graphics :: Graphics Conversion
@ -48,7 +49,7 @@ install_requires =
tinycss2>=1.0.0
cssselect2>=0.1
CairoSVG>=2.4.0
Pyphen>=0.8
Pyphen>=0.9.1
tests_require =
pytest-runner
pytest-cov

View File

@ -497,7 +497,10 @@ def _content_list(computer, values):
elif value[0] == 'attr()':
assert value[1][1] == 'string'
computed_value = compute_attr_function(computer, value)
elif value[0] in ('counter()', 'counters()', 'content()', 'string()'):
elif value[0] in (
'counter()', 'counters()', 'content()', 'element()',
'string()',
):
# Other values need layout context, their computed value cannot be
# better than their specified value yet.
# See build.compute_content_list.

View File

@ -743,3 +743,7 @@ def get_content_list_token(token, base_url):
elif arg.type == 'string':
string = arg.value
return ('leader()', ('string', string))
elif name == 'element':
if len(args) != 1 or args[0].type != 'ident':
return
return ('element()', args[0])

View File

@ -967,10 +967,15 @@ def text_overflow(keyword):
@property()
@single_keyword
def position(keyword):
@single_token
def position(token):
"""``position`` property validation."""
return keyword in ('static', 'relative', 'absolute', 'fixed')
if token.type == 'function' and token.name == 'running':
if len(token.arguments) == 1 and token.arguments[0].type == 'ident':
return ('running()', token.arguments[0].value)
keyword = get_single_keyword([token])
if keyword in ('static', 'relative', 'absolute', 'fixed'):
return keyword
@property()
@ -1364,7 +1369,7 @@ def string_set(tokens, base_url):
if None not in parsed_tokens:
return (var_name, parsed_tokens)
elif tokens and get_keyword(tokens[0]) == 'none':
return 'none'
return 'none', ()
@property()

View File

@ -88,8 +88,9 @@ class Box(object):
def all_children(self):
return ()
def __init__(self, element_tag, style):
def __init__(self, element_tag, style, element):
self.element_tag = element_tag
self.element = element
self.style = style
self.remove_decoration_sides = set()
@ -101,7 +102,7 @@ class Box(object):
"""Return an anonymous box that inherits from ``parent``."""
style = computed_from_cascaded(
cascaded={}, parent_style=parent.style, element=None)
return cls(parent.element_tag, style, *args, **kwargs)
return cls(parent.element_tag, style, parent.element, *args, **kwargs)
def copy(self):
"""Return shallow copy of the box."""
@ -113,6 +114,10 @@ class Box(object):
new_box.__dict__.update(self.__dict__)
return new_box
def deepcopy(self):
"""Return a copy of the box with recursive copies of its children."""
return self.copy()
def translate(self, dx=0, dy=0, ignore_floats=False):
"""Change the boxs position.
@ -274,9 +279,15 @@ class Box(object):
"""Return whether this box is in the absolute positioning scheme."""
return self.style['position'] in ('absolute', 'fixed')
def is_running(self):
"""Return whether this box is a running element."""
return self.style['position'][0] == 'running()'
def is_in_normal_flow(self):
"""Return whether this box is in normal flow."""
return not (self.is_floated() or self.is_absolutely_positioned())
return not (
self.is_floated() or self.is_absolutely_positioned() or
self.is_running())
# Start and end page values for named pages
@ -287,8 +298,8 @@ class Box(object):
class ParentBox(Box):
"""A box that has children."""
def __init__(self, element_tag, style, children):
super(ParentBox, self).__init__(element_tag, style)
def __init__(self, element_tag, style, element, children):
super(ParentBox, self).__init__(element_tag, style, element)
self.children = tuple(children)
def all_children(self):
@ -320,6 +331,11 @@ class ParentBox(Box):
return new_box
def deepcopy(self):
result = self.copy()
result.children = tuple(child.deepcopy() for child in self.children)
return result
def descendants(self):
"""A flat generator for a box, its children and descendants."""
yield self
@ -450,9 +466,9 @@ class TextBox(InlineLevelBox):
ascii_to_wide = dict((i, chr(i + 0xfee0)) for i in range(0x21, 0x7f))
ascii_to_wide.update({0x20: '\u3000', 0x2D: '\u2212'})
def __init__(self, element_tag, style, text):
def __init__(self, element_tag, style, element, text):
assert text
super(TextBox, self).__init__(element_tag, style)
super(TextBox, self).__init__(element_tag, style, element)
text_transform = style['text_transform']
if text_transform != 'none':
text = {
@ -500,8 +516,8 @@ class ReplacedBox(Box):
and is opaque from CSSs point of view.
"""
def __init__(self, element_tag, style, replacement):
super(ReplacedBox, self).__init__(element_tag, style)
def __init__(self, element_tag, style, element, replacement):
super(ReplacedBox, self).__init__(element_tag, style, element)
self.replacement = replacement
@ -651,7 +667,7 @@ class PageBox(ParentBox):
self.page_type = page_type
# Page boxes are not linked to any element.
super(PageBox, self).__init__(
element_tag=None, style=style, children=[])
element_tag=None, style=style, element=None, children=[])
def __repr__(self):
return '<%s %s>' % (type(self).__name__, self.page_type)
@ -663,7 +679,7 @@ class MarginBox(BlockContainerBox):
self.at_keyword = at_keyword
# Margin boxes are not linked to any element.
super(MarginBox, self).__init__(
element_tag=None, style=style, children=[])
element_tag=None, style=style, element=None, children=[])
def __repr__(self):
return '<%s %s>' % (type(self).__name__, self.at_keyword)

View File

@ -80,9 +80,10 @@ def build_formatting_structure(element_tree, style_for, get_image_from_uri,
return box
def make_box(element_tag, style, content):
return BOX_TYPE_FROM_DISPLAY[style['display']](
element_tag, style, content)
def make_box(element_tag, style, content, element):
box = BOX_TYPE_FROM_DISPLAY[style['display']](
element_tag, style, element, content)
return box
def element_to_box(element, style_for, get_image_from_uri, base_url,
@ -122,7 +123,7 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
if display == 'none':
return []
box = make_box(element.tag, style, [])
box = make_box(element.tag, style, [], element)
if state is None:
# use a list to have a shared mutable object
@ -227,7 +228,7 @@ def before_after_to_box(element, pseudo_type, state, style_for,
if 'none' in (display, content) or content in ('normal', 'inhibit'):
return []
box = make_box('%s::%s' % (element.tag, pseudo_type), style, [])
box = make_box('%s::%s' % (element.tag, pseudo_type), style, [], element)
quote_depth, counter_values, _counter_scopes = state
update_counters(state, style)
@ -264,7 +265,7 @@ def marker_to_box(element, state, parent_style, style_for, get_image_from_uri,
# `content` where 'normal' computes as 'inhibit' for pseudo elements.
quote_depth, counter_values, _counter_scopes = state
box = make_box('%s::marker' % element.tag, style, children)
box = make_box('%s::marker' % element.tag, style, children, element)
if style['display'] == 'none':
return
@ -492,6 +493,26 @@ def compute_content_list(content_list, parent_box, counter_values, css_token,
texts.append(quotes[min(quote_depth[0], len(quotes) - 1)])
if is_open:
quote_depth[0] += 1
elif type_ == 'element()':
if value.value not in context.running_elements:
# TODO: emit warning
continue
new_box = None
for i in range(context.current_page - 1, -1, -1):
if i not in context.running_elements[value.value]:
continue
running_box = context.running_elements[value.value][i]
new_box = running_box.deepcopy()
break
new_box.style['position'] = 'static'
for child in new_box.descendants():
if child.style['content'] in ('normal', 'none'):
continue
child.children = content_to_boxes(
child.style, child, quote_depth, counter_values,
get_image_from_uri, target_collector, context=context,
page=page)
boxlist.append(new_box)
text = ''.join(texts)
if text:
boxlist.append(boxes.TextBox.anonymous_from(parent_box, text))
@ -1247,7 +1268,7 @@ def inline_in_block(box):
]
"""
if not isinstance(box, boxes.ParentBox):
if not isinstance(box, boxes.ParentBox) or box.is_running():
return box
box_children = list(box.children)
@ -1382,7 +1403,7 @@ def block_in_inline(box):
]
"""
if not isinstance(box, boxes.ParentBox):
if not isinstance(box, boxes.ParentBox) or box.is_running():
return box
new_children = []

View File

@ -111,7 +111,7 @@ def make_replaced_box(element, box, image):
else:
# TODO: support images with 'display: table-cell'?
type_ = boxes.InlineReplacedBox
new_box = type_(element.tag, box.style, image)
new_box = type_(element.tag, box.style, element, image)
# TODO: check other attributes that need to be copied
# TODO: find another solution
new_box.string_set = box.string_set

View File

@ -196,6 +196,7 @@ class LayoutContext(object):
self._excluded_shapes_lists = []
self.excluded_shapes = None # Not initialized yet
self.string_set = defaultdict(lambda: defaultdict(lambda: list()))
self.running_elements = {}
self.current_page = None
self.forced_break = False

View File

@ -259,13 +259,6 @@ def block_container_layout(context, box, max_position_y, skip_stack,
# block_container_layout, there's probably a better solution.
assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox))
# TODO: this should make a difference, but that is currently neglected.
# See http://www.w3.org/TR/CSS21/visudet.html#normal-block
# http://www.w3.org/TR/CSS21/visudet.html#root-height
# if box.style['overflow'] != 'visible':
# ...
# We have to work around floating point rounding errors here.
# The 1e-9 value comes from PEP 485.
allowed_max_position_y = max_position_y * (1 + 1e-9)
@ -357,6 +350,10 @@ def block_container_layout(context, box, max_position_y, skip_stack,
break
resume_at = (index, None)
break
elif child.is_running():
context.running_elements.setdefault(
child.style['position'][1], {}
)[context.current_page - 1] = child
continue
if isinstance(child, boxes.LineBox):

View File

@ -858,11 +858,27 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
# add the original skip stack to the partial
# skip stack we get after the new rendering.
# We have to do:
# resume_at + initial_skip_stack
# but adding skip stacks is a bit complicated
current_skip_stack = initial_skip_stack
current_resume_at = (child_index, child_resume_at)
# Combining skip stacks is a bit complicated
# We have to:
# - set `child_index` as the first number
# - append the new stack if it's an absolute one
# - otherwise append the combined stacks
# (resume_at + initial_skip_stack)
# extract the initial index
if initial_skip_stack is None:
current_skip_stack = None
initial_index = 0
else:
initial_index, current_skip_stack = (
initial_skip_stack)
# child_resume_at is an absolute skip stack
if child_index > initial_index:
resume_at = (child_index, child_resume_at)
break
# combine the stacks
current_resume_at = child_resume_at
stack = []
while current_skip_stack and current_resume_at:
skip, current_skip_stack = (
@ -875,6 +891,8 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
resume_at = current_resume_at
while stack:
resume_at = (stack.pop(), resume_at)
# insert the child index
resume_at = (child_index, resume_at)
break
if break_found:
break

View File

@ -345,10 +345,11 @@ def make_margin_boxes(context, page, state):
box.style, box, quote_depth, counter_values,
context.get_image_from_uri, context.target_collector, context,
page)
# content_to_boxes() only produces inline-level boxes, no need to
# run other post-processors from build.build_formatting_structure()
box = build.inline_in_block(box)
build.process_whitespace(box)
box = build.anonymous_table_boxes(box)
box = build.flex_boxes(box)
box = build.inline_in_block(box)
box = build.block_in_inline(box)
resolve_percentages(box, containing_block)
if not box.is_generated:
box.width = box.height = 0

View File

@ -182,7 +182,7 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block,
else:
row.height = max(row.height, max(
row_cell.height for row_cell in ending_cells))
row_bottom_y = cell.position_y + row.height
row_bottom_y = row.position_y + row.height
else:
row_bottom_y = row.position_y
row.height = 0

View File

@ -382,6 +382,59 @@ def test_breaking_linebox_regression_10():
assert line4.children[0].text == ')x'
@assert_no_logs
def test_breaking_linebox_regression_11():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/953
page, = parse(
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<p style="width:10em; font-family: ahem">'
' line 1<br><span>123 567 90</span>x'
'</p>')
html, = page.children
body, = html.children
p, = body.children
line1, line2, line3 = p.children
assert line1.children[0].text == 'line 1'
assert line2.children[0].children[0].text == '123 567'
assert line3.children[0].children[0].text == '90'
assert line3.children[1].text == 'x'
@assert_no_logs
def test_breaking_linebox_regression_12():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/953
page, = parse(
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<p style="width:10em; font-family: ahem">'
' <br><span>123 567 90</span>x'
'</p>')
html, = page.children
body, = html.children
p, = body.children
line1, line2, line3 = p.children
assert line2.children[0].children[0].text == '123 567'
assert line3.children[0].children[0].text == '90'
assert line3.children[1].text == 'x'
@assert_no_logs
def test_breaking_linebox_regression_13():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/953
page, = parse(
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<p style="width:10em; font-family: ahem">'
' 123 567 90 <span>123 567 90</span>x'
'</p>')
html, = page.children
body, = html.children
p, = body.children
line1, line2, line3 = p.children
assert line1.children[0].text == '123 567 90'
assert line2.children[0].children[0].text == '123 567'
assert line3.children[0].children[0].text == '90'
assert line3.children[1].text == 'x'
@assert_no_logs
def test_linebox_text():
page, = parse('''

View File

@ -1214,3 +1214,62 @@ def test_margin_boxes_vertical_align():
assert line_1.position_y == 3
assert line_2.position_y == 43
assert line_3.position_y == 83
@assert_no_logs
def test_margin_boxes_element():
pages = render_pages('''
<html>
<head>
<style type="text/css">
.footer {
position: running(footer);
}
@page {
@bottom-center {
content: element(footer);
}
}
h1 {
margin-bottom: 15cm;
}
.page:before {
content: counter(page);
}
.pages:after {
content: counter(pages);
}
</style>
</head>
<body>
<div class="footer">
<span class="page" /> of <span class="pages" />
</div>
<h1>test1</h1>
<h1>test2</h1>
<h1>test3</h1>
<h1>test4</h1>
<h1>test5</h1>
<h1>test6</h1>
<div class="footer">
Last page will be a static footer
</div>
</body>
</html>
''')
# first footer
footer1_text = ''.join(
getattr(node, 'text', '')
for node in pages[0].children[1].descendants())
assert footer1_text == '1 of 3'
# second footer
footer2_text = ''.join(
getattr(node, 'text', '')
for node in pages[1].children[1].descendants())
assert footer2_text == '2 of 3'
# last footer
footer3_text = ''.join(
getattr(node, 'text', '')
for node in pages[2].children[1].descendants())
assert footer3_text == 'Last page will be a static footer'

View File

@ -147,6 +147,7 @@ ffi.cdef('''
void g_type_init (void);
void pango_layout_set_width (PangoLayout *layout, int width);
PangoAttrList * pango_layout_get_attributes(PangoLayout *layout);
void pango_layout_set_attributes (
PangoLayout *layout, PangoAttrList *attrs);
void pango_layout_set_text (
@ -766,7 +767,10 @@ class Layout(object):
if text and (word_spacing != 0 or letter_spacing != 0):
letter_spacing = units_from_double(letter_spacing)
space_spacing = units_from_double(word_spacing) + letter_spacing
attr_list = pango.pango_attr_list_new()
attr_list = pango.pango_layout_get_attributes(self.layout)
if not attr_list:
# TODO: list should be freed
attr_list = pango.pango_attr_list_new()
def add_attr(start, end, spacing):
# TODO: attributes should be freed
@ -781,7 +785,6 @@ class Layout(object):
position = bytestring.find(b' ', position + 1)
pango.pango_layout_set_attributes(self.layout, attr_list)
pango.pango_attr_list_unref(attr_list)
# Tabs width
if b'\t' in bytestring:

0
weasyprint/tools/renderer.py Executable file → Normal file
View File