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

Use index stacks instead of popleft for line and page breaks.

This commit is contained in:
Simon Sapin 2011-09-30 19:04:05 +02:00
parent 3729c7e5d3
commit cc85f95c5b
5 changed files with 170 additions and 243 deletions

View File

@ -406,13 +406,16 @@ def _inner_block_in_inline(box, skip_stack=None):
for index, child in box.enumerate_skip(skip):
if isinstance(child, boxes.BlockLevelBox):
assert skip_stack is None # Should no skip here
block_level_box = child
index += 1 # Resume *after* the block
else:
if isinstance(child, boxes.InlineBox):
recursion = _inner_block_in_inline(child, skip_stack)
skip_stack = None
new_child, block_level_box, resume_at = recursion
else:
assert skip_stack is None # Should no skip here
if isinstance(child, boxes.ParentBox):
# inline-block or inline-table.
new_child = block_in_inline(child)

View File

@ -30,7 +30,7 @@ from ..css.values import get_pixel_value
from ..formatting_structure import boxes
def make_page(document, page_number):
def make_page(document, page_number, resume_at):
"""Take just enough content from the beginning to fill one page.
Return ``page, finished``. ``page`` is a laid out Page object, ``finished``
@ -58,9 +58,10 @@ def make_page(document, page_number):
# TODO: handle cases where the root element is something else.
# See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
assert isinstance(root_box, boxes.BlockBox)
page.root_box, finished = block_box_layout(root_box, page_content_bottom)
page.root_box, resume_at = block_box_layout(
root_box, page_content_bottom, resume_at)
return page, finished
return page, resume_at
def layout(document):
@ -75,9 +76,10 @@ def layout(document):
"""
pages = []
page_number = 1
resume_at = None
while True:
page, finished = make_page(document, page_number)
page, resume_at = make_page(document, page_number, resume_at)
pages.append(page)
if finished:
if resume_at is None:
return pages
page_number += 1

View File

@ -30,7 +30,7 @@ from ..css.values import get_single_keyword
from ..formatting_structure import boxes
def block_level_layout(box, max_position_y):
def block_level_layout(box, max_position_y, skip_stack):
"""Lay out the block-level ``box``.
:param max_position_y: the absolute vertical position (as in
@ -39,19 +39,19 @@ def block_level_layout(box, max_position_y):
"""
if isinstance(box, boxes.BlockBox):
return block_box_layout(box, max_position_y)
return block_box_layout(box, max_position_y, skip_stack)
elif isinstance(box, boxes.BlockLevelReplacedBox):
return block_replaced_box_layout(box), True
return block_replaced_box_layout(box), None
else:
raise TypeError('Layout for %s not handled yet' % type(box).__name__)
def block_box_layout(box, max_position_y):
def block_box_layout(box, max_position_y, skip_stack):
"""Lay out the block ``box``."""
resolve_percentages(box)
block_level_width(box)
list_marker_layout(box)
return block_level_height(box, max_position_y)
return block_level_height(box, max_position_y, skip_stack)
def block_replaced_box_layout(box):
@ -133,7 +133,7 @@ def block_level_width(box):
box.margin_right = margin_sum - margin_l
def block_level_height(box, max_position_y):
def block_level_height(box, max_position_y, skip_stack):
"""Set the ``box`` height."""
assert isinstance(box, boxes.BlockBox)
@ -151,8 +151,13 @@ def block_level_height(box, max_position_y):
new_box = box.copy()
new_box.empty()
while box.children:
child = box.children.popleft()
if skip_stack is None:
skip = 0
else:
skip, skip_stack = skip_stack
for index, child in box.enumerate_skip(skip):
if not child.is_in_normal_flow():
continue
# TODO: collapse margins
@ -160,24 +165,31 @@ def block_level_height(box, max_position_y):
child.position_x = position_x
child.position_y = position_y
if isinstance(child, boxes.LineBox):
for line in get_new_lineboxes(child, max_position_y):
lines, resume_at = get_new_lineboxes(
child, max_position_y, skip_stack)
skip_stack = None
for line in lines:
new_box.add_child(line)
position_y += line.height
if child.children:
box.children.appendleft(child)
if resume_at is not None:
resume_at = (index, resume_at)
break
else:
new_child, finished = block_level_layout(child, max_position_y)
new_child, resume_at = block_level_layout(
child, max_position_y, skip_stack)
skip_stack = None
new_position_y = position_y + new_child.margin_height()
# TODO: find a way to break between blocks
# if new_position_y <= max_position_y:
new_box.add_child(new_child)
position_y = new_position_y
# else:
# finished = False
if not finished:
box.children.appendleft(child)
# resume_at = (index, None) # or something... XXX
if resume_at is not None:
resume_at = (index, resume_at)
break
else:
resume_at = None
if new_box.height == 'auto':
new_box.height = position_y - initial_position_y
@ -185,5 +197,4 @@ def block_level_height(box, max_position_y):
# If there was a list marker, we kept it on `new_box`. Do not repeat on
# `box` on the next page.
box.outside_list_marker = None
finished = not box.children
return new_box, finished
return new_box, resume_at

View File

@ -31,74 +31,38 @@ from ..formatting_structure import boxes
from ..css.values import get_single_keyword, get_single_pixel_value
class InlineContext(object):
"""Context manager for inline boxes."""
def __init__(self, linebox, page_bottom):
self.linebox = linebox
self.page_bottom = page_bottom
self.position_y = linebox.position_y
self.position_x = linebox.position_x
self.containing_block_width = linebox.containing_block_size()[0]
self.lines = []
self.execute_formatting()
def deep_copy(self, box):
"""Copy a ``box`` and its children recursively."""
copy_box = box.copy()
if isinstance(box, boxes.ParentBox):
copy_box.empty()
for child in box.children:
if isinstance(box, boxes.ParentBox):
copy_child = self.deep_copy(child)
else:
copy_child = child.copy()
copy_box.add_child(copy_child)
return copy_box
else:
return copy_box
def save(self, line):
"""Save the line and the position_y."""
self.copy_line = self.deep_copy(line)
self._position_y = self.position_y
def restore(self):
"""Restore the linebox children."""
for child in self.copy_line.children:
self.linebox.children.appendleft(child)
child.parent = self.linebox
self.position_y = self._position_y
def execute_formatting(self):
"""Break the lines until the bottom of the page is reached."""
first = True
while 1:
line = layout_next_linebox(
self.linebox, self.containing_block_width)
if line is None:
break
self.save(line)
white_space_processing(line)
compute_linebox_dimensions(line)
compute_linebox_positions(line, self.position_x, self.position_y)
vertical_align_processing(line)
compute_linebox_dimensions(line)
if not is_empty_line(line):
self.position_y += line.height
# Yield at least one line to avoid infinite loop.
# TODO: Find another way ...
if self.page_bottom >= self.position_y or first:
self.lines.append(line)
first = False
else:
self.restore()
break
def get_new_lineboxes(linebox, page_bottom):
def get_new_lineboxes(linebox, page_bottom, skip_stack):
"""Get the ``linebox`` lines until ``page_bottom`` is reached."""
inline_context = InlineContext(linebox, page_bottom)
return inline_context.lines
first = True
position_y = linebox.position_y
position_x = linebox.position_x
containing_block_width = linebox.containing_block_size()[0]
lines = []
while 1:
line, resume_at = layout_next_linebox(
linebox, containing_block_width, skip_stack)
from ..tests.test_boxes import serialize, prettify
white_space_processing(line)
compute_linebox_dimensions(line)
compute_linebox_positions(line, position_x, position_y)
vertical_align_processing(line)
compute_linebox_dimensions(line) # XXX twice?
if not is_empty_line(line):
position_y += line.height
# Yield at least one line to avoid infinite loop.
# TODO: Find another way ...
if page_bottom >= position_y or first:
lines.append(line)
first = False
else:
# Resume before this line
resume_at = skip_stack
break
if resume_at is None:
break
else:
skip_stack = resume_at
return lines, resume_at
def inline_replaced_box_layout(box):
@ -305,122 +269,73 @@ def get_new_empty_line(linebox):
return new_line
def layout_next_linebox(linebox, allocate_width):
"""Cut the ``linebox`` to fit in ``alocate_width``.
Eg.::
LineBox[
InlineBox[
TextBox('Hello.'),
],
InlineBox[
InlineBox[TextBox('Word :D')],
TextBox('This is a long long long text'),
]
]
is turned into::
[
LineBox[
InlineBox[
TextBox('Hello.'),
],
InlineBox[
InlineBox[TextBox('Word :D')],
TextBox('This is a long'),
]
], LineBox[
InlineBox[
TextBox(' long long text'),
]
]
]
"""
def layout_next_linebox(linebox, remaining_width, skip_stack):
"""Same as split_inline_level."""
assert isinstance(linebox, boxes.LineBox)
new_line = get_new_empty_line(linebox)
remaining_width = allocate_width
while linebox.children:
child = linebox.children.popleft()
part1, part2 = split_inline_level(child, remaining_width)
assert part1 is not None
if skip_stack is None:
skip = 0
else:
skip, skip_stack = skip_stack
if part1.margin_width() > remaining_width and new_line.children:
# part1 is too wide, and the line is non-empty:
for index, child in linebox.enumerate_skip(skip):
new_child, resume_at = split_inline_level(
child, remaining_width, skip_stack)
skip_stack = None
margin_width = new_child.margin_width()
if margin_width > remaining_width and new_line.children:
# too wide, and the inline is non-empty:
# put child entirely on the next line.
if part2 is not None:
linebox.children.appendleft(part2)
part2 = part1
resume_at = (index, None)
break
else:
remaining_width -= part1.margin_width()
new_line.add_child(part1)
remaining_width -= margin_width
new_line.add_child(new_child)
if part2 is not None:
linebox.children.appendleft(part2)
# This line is done, create a new one and reset
# the available width.
return new_line
if resume_at is not None:
resume_at = (index, resume_at)
break
else:
resume_at = None
if new_line.children:
return new_line
return new_line, resume_at
def split_inline_level(box, available_width):
"""Split an inline-level box and return ``(part1, part2)``.
def split_inline_level(box, available_width, skip_stack):
"""Fit as much content as possible from an inline-level box in a width.
* The first part is non-empty (unless the box is empty)
* Have the first part as big as possible while being narrower than
``available_width``, if possible (may overflow is no split is possible.)
* ``part2`` may be None.
Return ``(new_box, resume_at)``. ``resume_at`` is ``None`` if all of the
content fits. Otherwise it can be passed as a ``skip_stack`` parameter
to resume where we left off.
``new_box`` is non-empty (unless the box is empty) and as big as possible
while being narrower than ``available_width``, if possible (may overflow
is no split is possible.)
"""
if isinstance(box, boxes.TextBox):
part1, part2 = split_text_box(box, available_width)
compute_textbox_dimensions(part1)
new_box, resume_at = split_text_box(box, available_width, skip_stack)
compute_textbox_dimensions(new_box)
elif isinstance(box, boxes.InlineBox):
resolve_percentages(box)
if box.margin_left == 'auto':
box.margin_left = 0
if box.margin_right == 'auto':
box.margin_right = 0
part1, part2 = split_inline_box(box, available_width)
compute_inlinebox_dimensions(part1)
new_box, resume_at = split_inline_box(box, available_width, skip_stack)
compute_inlinebox_dimensions(new_box)
elif isinstance(box, boxes.AtomicInlineLevelBox):
compute_atomicbox_dimensions(box)
part1 = box
part2 = None
else:
assert False, box
return part1, part2
new_box = box
resume_at = None
#else: unexpected box type here
return new_box, resume_at
def split_inline_box(inlinebox, remaining_width):
"""Split an inline box and return ``(part1, part2)``.
The same rules as split_inline_box() apply.
Eg.::
InlineBox[
InlineBox[TextBox('Word :D')],
TextBox('This is a long long long text'),
]
is turned into::
(
InlineBox[
InlineBox[TextBox('Word :D')],
TextBox('This is a long'),
], InlineBox[
TextBox(' long long text'),
]
)
"""
def split_inline_box(inlinebox, remaining_width, skip_stack):
"""Same behavior as split_inline_level."""
assert isinstance(inlinebox, boxes.InlineBox)
resolve_percentages(inlinebox)
left_spacing = (inlinebox.padding_left + inlinebox.margin_left +
@ -432,86 +347,75 @@ def split_inline_box(inlinebox, remaining_width):
new_inlinebox = inlinebox.copy()
new_inlinebox.empty()
while inlinebox.children:
child = inlinebox.children.popleft()
if skip_stack is None:
skip = 0
else:
skip, skip_stack = skip_stack
part1, part2 = split_inline_level(child, remaining_width)
assert part1 is not None
for index, child in inlinebox.enumerate_skip(skip):
new_child, resume_at = split_inline_level(
child, remaining_width, skip_stack)
skip_stack = None
# TODO: this is non-optimal when last_child is True and
# width <= remaining_width < width + right_spacing
# with
# width = part1.margin_width()
last_child = not inlinebox.children # ie. the list is now empty
if last_child:
pass
# TODO: take care of right_spacing
# TODO: on the last child, take care of right_spacing
if part1.margin_width() > remaining_width and new_inlinebox.children:
# part1 is too wide, and the inline is non-empty:
margin_width = new_child.margin_width()
if (margin_width > remaining_width and new_inlinebox.children):
# too wide, and the inline is non-empty:
# put child entirely on the next line.
if part2 is not None:
inlinebox.children.appendleft(part2)
part2 = part1
resume_at = (index, None)
break
else:
remaining_width -= part1.margin_width()
new_inlinebox.add_child(part1)
remaining_width -= margin_width
new_inlinebox.add_child(new_child)
if part2 is not None:
inlinebox.children.appendleft(part2)
if resume_at is not None:
inlinebox.reset_spacing('left')
new_inlinebox.reset_spacing('right')
return new_inlinebox, inlinebox
resume_at = (index, resume_at)
break
else:
resume_at = None
return new_inlinebox, None
return new_inlinebox, resume_at
def split_text_box(textbox, allocate_width):
"""Split a text box and return ``(part1, part2)``.
def split_text_box(textbox, available_width, skip):
"""Keep as much text as possible from a TextBox in a limitied width.
Try not to overflow but always have some text in ``new_textbox``
The same rules as split_inline_box() apply, but the text will also be
split at preserved newline characters.
Return ``(new_textbox, skip)``. ``skip`` is the number of UTF-8 bytes
to skip form the start of the TextBox for the next line, or ``None``
if all of the text fits.
Eg.::
TextBox('This is a long long long long text')
is turned into::
(
TextBox('This is a long long'),
TextBox(' long long text')
)
But::
TextBox('Thisisalonglonglonglongtext')
is turned into::
(
TextBox('Thisisalonglonglonglongtext'),
None
)
Also break an preserved whitespace.
"""
assert isinstance(textbox, boxes.TextBox)
font_size = get_single_pixel_value(textbox.style.font_size)
if font_size == 0:
return textbox, None
fragment = TextFragment(textbox.utf8_text, textbox.style,
width=allocate_width,
context=cairo.Context(textbox.document.surface))
skip = skip or 0
utf8_text = textbox.utf8_text[skip:]
fragment = TextFragment(utf8_text, textbox.style,
cairo.Context(textbox.document.surface), available_width)
split = fragment.split_first_line()
if split is None:
if skip:
textbox = textbox.copy()
textbox.utf8_text = utf8_text # with skip
return textbox, None
first_end, second_start = split
first_tb = textbox.copy()
first_tb.utf8_text = textbox.utf8_text[:first_end]
second_tb = textbox.copy()
second_tb.utf8_text = textbox.utf8_text[second_start:]
return first_tb, second_tb
new_textbox = textbox.copy()
new_textbox.utf8_text = utf8_text[:first_end]
return new_textbox, skip + second_start
def white_space_processing(linebox):

View File

@ -463,7 +463,7 @@ def test_linebox_text():
</style>
<p><em>Lorem Ipsum</em>is very <strong>coool</strong></p>'''
page, = parse(page % {'fonts': FONTS, 'width': 200})
page, = parse(page % {'fonts': FONTS, 'width': 250})
paragraph, = body_children(page)
return paragraph
@ -488,7 +488,7 @@ def test_linebox_positions():
p { width:%(width)spx; font-family:%(fonts)s;}
</style>
<p>this is test for <strong>Weasyprint</strong></p>'''
page, = parse(page % {'fonts': FONTS, 'width': 200})
page, = parse(page % {'fonts': FONTS, 'width': 250})
paragraph, = body_children(page)
return paragraph
@ -579,9 +579,12 @@ def test_inlinebox_spliting():
def get_parts(inlinebox, width):
"""Yield the parts of the splitted ``inlinebox`` of given ``width``."""
copy_inlinebox = inlinebox.copy()
while copy_inlinebox.children:
yield split_inline_box(copy_inlinebox, width)[0]
skip = None
while 1:
box, skip = split_inline_box(inlinebox, width, skip)
yield box
if skip is None:
break
def get_joined_text(parts):
"""Get the joined text from ``parts``."""
@ -616,7 +619,7 @@ def test_inlinebox_spliting():
# test with width = 100
parts = list(get_parts(inlinebox, 100))
assert len(parts) != 1
assert len(parts) > 1
assert original_text == get_joined_text(parts)
inlinebox = get_inlinebox(content)
@ -625,7 +628,7 @@ def test_inlinebox_spliting():
# test with width = 10
parts = list(get_parts(inlinebox, 10))
assert len(parts) != 1
assert len(parts) > 1
assert original_text == get_joined_text(parts)
# with margin-border-padding
@ -678,8 +681,12 @@ def test_inlinebox_text_after_spliting():
def get_parts(inlinebox, width):
"""Yield the parts of the splitted ``inlinebox`` of given ``width``."""
while inlinebox.children:
yield split_inline_box(inlinebox, width)[0]
skip = None
while 1:
box, skip = split_inline_box(inlinebox, width, skip)
yield box
if skip is None:
break
def get_full_text(inlinebox):
"""Get the full text in ``inlinebox``."""
@ -743,7 +750,7 @@ def test_page_and_linebox_breaking():
texts.extend(get_full_text(lines))
return u' '.join(texts)
content = '1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15'
content = u'1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15'
pages = get_pages(content)
assert len(pages) == 2