"""Layout for pages and CSS3 margin boxes.""" import copy from collections import namedtuple from math import inf from ..css import computed_from_cascaded from ..formatting_structure import boxes, build from ..logger import PROGRESS_LOGGER from .absolute import absolute_box_layout, absolute_layout from .block import block_container_layout, block_level_layout from .float import float_layout from .min_max import handle_min_max_height, handle_min_max_width from .percent import resolve_percentages from .preferred import max_content_width, min_content_width PageType = namedtuple('PageType', ['side', 'blank', 'name', 'index', 'group_index']) class OrientedBox: @property def sugar(self): return self.padding_plus_border + self.margin_a + self.margin_b @property def outer(self): return self.sugar + self.inner @outer.setter def outer(self, new_outer_width): self.inner = min( max(self.min_content_size, new_outer_width - self.sugar), self.max_content_size) @property def outer_min_content_size(self): return self.sugar + ( self.min_content_size if self.inner == 'auto' else self.inner) @property def outer_max_content_size(self): return self.sugar + ( self.max_content_size if self.inner == 'auto' else self.inner) class VerticalBox(OrientedBox): def __init__(self, context, box): self.context = context self.box = box # Inner dimension: that of the content area, as opposed to the # outer dimension: that of the margin area. self.inner = box.height self.margin_a = box.margin_top self.margin_b = box.margin_bottom self.padding_plus_border = ( box.padding_top + box.padding_bottom + box.border_top_width + box.border_bottom_width) def restore_box_attributes(self): box = self.box box.height = self.inner box.margin_top = self.margin_a box.margin_bottom = self.margin_b # TODO: Define what are the min-content and max-content heights @property def min_content_size(self): return 0 @property def max_content_size(self): return 1e6 class HorizontalBox(OrientedBox): def __init__(self, context, box): self.context = context self.box = box self.inner = box.width self.margin_a = box.margin_left self.margin_b = box.margin_right self.padding_plus_border = ( box.padding_left + box.padding_right + box.border_left_width + box.border_right_width) self._min_content_size = None self._max_content_size = None def restore_box_attributes(self): box = self.box box.width = self.inner box.margin_left = self.margin_a box.margin_right = self.margin_b @property def min_content_size(self): if self._min_content_size is None: self._min_content_size = min_content_width( self.context, self.box, outer=False) return self._min_content_size @property def max_content_size(self): if self._max_content_size is None: self._max_content_size = max_content_width( self.context, self.box, outer=False) return self._max_content_size def compute_fixed_dimension(context, box, outer, vertical, top_or_left): """Compute and set a margin box fixed dimension on ``box``. Described in: https://drafts.csswg.org/css-page-3/#margin-constraints :param box: The margin box to work on :param outer: The target outer dimension (value of a page margin) :param vertical: True to set height, margin-top and margin-bottom; False for width, margin-left and margin-right :param top_or_left: True if the margin box in if the top half (for vertical==True) or left half (for vertical==False) of the page. This determines which margin should be 'auto' if the values are over-constrained. (Rule 3 of the algorithm.) """ box = (VerticalBox if vertical else HorizontalBox)(context, box) # Rule 2 total = box.padding_plus_border + sum( value for value in (box.margin_a, box.margin_b, box.inner) if value != 'auto') if total > outer: if box.margin_a == 'auto': box.margin_a = 0 if box.margin_b == 'auto': box.margin_b = 0 if box.inner == 'auto': # XXX this is not in the spec, but without it box.inner # would end up with a negative value. # Instead, this will trigger rule 3 below. # https://lists.w3.org/Archives/Public/www-style/2012Jul/0006.html box.inner = 0 # Rule 3 if 'auto' not in [box.margin_a, box.margin_b, box.inner]: # Over-constrained if top_or_left: box.margin_a = 'auto' else: box.margin_b = 'auto' # Rule 4 if [box.margin_a, box.margin_b, box.inner].count('auto') == 1: if box.inner == 'auto': box.inner = (outer - box.padding_plus_border - box.margin_a - box.margin_b) elif box.margin_a == 'auto': box.margin_a = (outer - box.padding_plus_border - box.margin_b - box.inner) elif box.margin_b == 'auto': box.margin_b = (outer - box.padding_plus_border - box.margin_a - box.inner) # Rule 5 if box.inner == 'auto': if box.margin_a == 'auto': box.margin_a = 0 if box.margin_b == 'auto': box.margin_b = 0 box.inner = (outer - box.padding_plus_border - box.margin_a - box.margin_b) # Rule 6 if box.margin_a == box.margin_b == 'auto': box.margin_a = box.margin_b = ( outer - box.padding_plus_border - box.inner) / 2 assert 'auto' not in [box.margin_a, box.margin_b, box.inner] box.restore_box_attributes() def compute_variable_dimension(context, side_boxes, vertical, available_size): """Compute and set a margin box fixed dimension on ``box`` Described in: https://drafts.csswg.org/css-page-3/#margin-dimension :param side_boxes: Three boxes on a same side (as opposed to a corner). A list of: - A @*-left or @*-top margin box - A @*-center or @*-middle margin box - A @*-right or @*-bottom margin box :param vertical: ``True`` to set height, margin-top and margin-bottom; ``False`` for width, margin-left and margin-right. :param available_size: The distance between the page box’s left right border edges """ box_class = VerticalBox if vertical else HorizontalBox side_boxes = [box_class(context, box) for box in side_boxes] box_a, box_b, box_c = side_boxes for box in side_boxes: if box.margin_a == 'auto': box.margin_a = 0 if box.margin_b == 'auto': box.margin_b = 0 if not box_b.box.is_generated: # Non-generated boxes get zero for every box-model property assert box_b.inner == 0 if box_a.inner == box_c.inner == 'auto': # A and C both have 'width: auto' if available_size > ( box_a.outer_max_content_size + box_c.outer_max_content_size): # sum of the outer max-content widths # is less than the available width flex_space = ( available_size - box_a.outer_max_content_size - box_c.outer_max_content_size) flex_factor_a = box_a.outer_max_content_size flex_factor_c = box_c.outer_max_content_size flex_factor_sum = flex_factor_a + flex_factor_c if flex_factor_sum == 0: flex_factor_sum = 1 box_a.outer = box_a.max_content_size + ( flex_space * flex_factor_a / flex_factor_sum) box_c.outer = box_c.max_content_size + ( flex_space * flex_factor_c / flex_factor_sum) elif available_size > ( box_a.outer_min_content_size + box_c.outer_min_content_size): # sum of the outer min-content widths # is less than the available width flex_space = ( available_size - box_a.outer_min_content_size - box_c.outer_min_content_size) flex_factor_a = ( box_a.max_content_size - box_a.min_content_size) flex_factor_c = ( box_c.max_content_size - box_c.min_content_size) flex_factor_sum = flex_factor_a + flex_factor_c if flex_factor_sum == 0: flex_factor_sum = 1 box_a.outer = box_a.min_content_size + ( flex_space * flex_factor_a / flex_factor_sum) box_c.outer = box_c.min_content_size + ( flex_space * flex_factor_c / flex_factor_sum) else: # otherwise flex_space = ( available_size - box_a.outer_min_content_size - box_c.outer_min_content_size) flex_factor_a = box_a.min_content_size flex_factor_c = box_c.min_content_size flex_factor_sum = flex_factor_a + flex_factor_c if flex_factor_sum == 0: flex_factor_sum = 1 box_a.outer = box_a.min_content_size + ( flex_space * flex_factor_a / flex_factor_sum) box_c.outer = box_c.min_content_size + ( flex_space * flex_factor_c / flex_factor_sum) else: # only one box has 'width: auto' if box_a.inner == 'auto': box_a.outer = available_size - box_c.outer elif box_c.inner == 'auto': box_c.outer = available_size - box_a.outer else: if box_b.inner == 'auto': # resolve any auto width of the middle box (B) ac_max_content_size = 2 * max( box_a.outer_max_content_size, box_c.outer_max_content_size) if available_size > ( box_b.outer_max_content_size + ac_max_content_size): flex_space = ( available_size - box_b.outer_max_content_size - ac_max_content_size) flex_factor_b = box_b.outer_max_content_size flex_factor_ac = ac_max_content_size flex_factor_sum = flex_factor_b + flex_factor_ac if flex_factor_sum == 0: flex_factor_sum = 1 box_b.outer = box_b.max_content_size + ( flex_space * flex_factor_b / flex_factor_sum) else: ac_min_content_size = 2 * max( box_a.outer_min_content_size, box_c.outer_min_content_size) if available_size > ( box_b.outer_min_content_size + ac_min_content_size): flex_space = ( available_size - box_b.outer_min_content_size - ac_min_content_size) flex_factor_b = ( box_b.max_content_size - box_b.min_content_size) flex_factor_ac = ac_max_content_size - ac_min_content_size flex_factor_sum = flex_factor_b + flex_factor_ac if flex_factor_sum == 0: flex_factor_sum = 1 box_b.outer = box_b.min_content_size + ( flex_space * flex_factor_b / flex_factor_sum) else: flex_space = ( available_size - box_b.outer_min_content_size - ac_min_content_size) flex_factor_b = box_b.min_content_size flex_factor_ac = ac_min_content_size flex_factor_sum = flex_factor_b + flex_factor_ac if flex_factor_sum == 0: flex_factor_sum = 1 box_b.outer = box_b.min_content_size + ( flex_space * flex_factor_b / flex_factor_sum) if box_a.inner == 'auto': box_a.outer = (available_size - box_b.outer) / 2 if box_c.inner == 'auto': box_c.outer = (available_size - box_b.outer) / 2 # And, we’re done! assert 'auto' not in [box.inner for box in side_boxes] # Set the actual attributes back. for box in side_boxes: box.restore_box_attributes() def _standardize_page_based_counters(style, pseudo_type): """Drop 'pages' counter from style in @page and @margin context. Ensure `counter-increment: page` for @page context if not otherwise manipulated by the style. """ page_counter_touched = False for propname in ('counter_set', 'counter_reset', 'counter_increment'): if style[propname] == 'auto': style[propname] = () continue justified_values = [] for name, value in style[propname]: if name == 'page': page_counter_touched = True if name != 'pages': justified_values.append((name, value)) style[propname] = tuple(justified_values) if pseudo_type is None and not page_counter_touched: style['counter_increment'] = ( ('page', 1),) + style['counter_increment'] def make_margin_boxes(context, page, state): """Yield laid-out margin boxes for this page. ``state`` is the actual, up-to-date page-state from ``context.page_maker[context.current_page]``. """ # This is a closure only to make calls shorter def make_box(at_keyword, containing_block): """Return a margin box with resolved percentages. The margin box may still have 'auto' values. Return ``None`` if this margin box should not be generated. :param at_keyword: Which margin box to return, e.g. '@top-left' :param containing_block: As expected by :func:`resolve_percentages`. """ style = context.style_for(page.page_type, at_keyword) if style is None: # doesn't affect counters style = computed_from_cascaded( element=None, cascaded={}, parent_style=page.style) _standardize_page_based_counters(style, at_keyword) box = boxes.MarginBox(at_keyword, style) # Empty boxes should not be generated, but they may be needed for # the layout of their neighbors. # TODO: should be the computed value. box.is_generated = style['content'] not in ( 'normal', 'inhibit', 'none') # TODO: get actual counter values at the time of the last page break if box.is_generated: # @margins mustn't manipulate page-context counters margin_state = copy.deepcopy(state) quote_depth, counter_values, counter_scopes = margin_state # TODO: check this, probably useless counter_scopes.append(set()) build.update_counters(margin_state, box.style) box.children = build.content_to_boxes( box.style, box, quote_depth, counter_values, context.get_image_from_uri, context.target_collector, context.counter_style, context, page) build.process_whitespace(box) build.process_text_transform(box) box = build.create_anonymous_boxes(box) resolve_percentages(box, containing_block) if not box.is_generated: box.width = box.height = 0 for side in ('top', 'right', 'bottom', 'left'): box._reset_spacing(side) return box margin_top = page.margin_top margin_bottom = page.margin_bottom margin_left = page.margin_left margin_right = page.margin_right max_box_width = page.border_width() max_box_height = page.border_height() # bottom right corner of the border box page_end_x = margin_left + max_box_width page_end_y = margin_top + max_box_height # Margin box dimensions, described in # https://drafts.csswg.org/css-page-3/#margin-box-dimensions generated_boxes = [] for prefix, vertical, containing_block, position_x, position_y in ( ('top', False, (max_box_width, margin_top), margin_left, 0), ('bottom', False, (max_box_width, margin_bottom), margin_left, page_end_y), ('left', True, (margin_left, max_box_height), 0, margin_top), ('right', True, (margin_right, max_box_height), page_end_x, margin_top), ): if vertical: suffixes = ['top', 'middle', 'bottom'] fixed_outer, variable_outer = containing_block else: suffixes = ['left', 'center', 'right'] variable_outer, fixed_outer = containing_block side_boxes = [ make_box(f'@{prefix}-{suffix}', containing_block) for suffix in suffixes] if not any(box.is_generated for box in side_boxes): continue # We need the three boxes together for the variable dimension: compute_variable_dimension( context, side_boxes, vertical, variable_outer) for box, offset in zip(side_boxes, [0, 0.5, 1]): if not box.is_generated: continue box.position_x = position_x box.position_y = position_y if vertical: box.position_y += offset * ( variable_outer - box.margin_height()) else: box.position_x += offset * ( variable_outer - box.margin_width()) compute_fixed_dimension( context, box, fixed_outer, not vertical, prefix in ('top', 'left')) generated_boxes.append(box) # Corner boxes for at_keyword, cb_width, cb_height, position_x, position_y in ( ('@top-left-corner', margin_left, margin_top, 0, 0), ('@top-right-corner', margin_right, margin_top, page_end_x, 0), ('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y), ('@bottom-right-corner', margin_right, margin_bottom, page_end_x, page_end_y), ): box = make_box(at_keyword, (cb_width, cb_height)) if not box.is_generated: continue box.position_x = position_x box.position_y = position_y compute_fixed_dimension( context, box, cb_height, True, 'top' in at_keyword) compute_fixed_dimension( context, box, cb_width, False, 'left' in at_keyword) generated_boxes.append(box) for box in generated_boxes: yield margin_box_content_layout(context, page, box) def margin_box_content_layout(context, page, box): """Layout a margin box’s content once the box has dimensions.""" positioned_boxes = [] box, resume_at, next_page, _, _, _ = block_container_layout( context, box, bottom_space=-inf, skip_stack=None, page_is_empty=True, absolute_boxes=positioned_boxes, fixed_boxes=positioned_boxes, adjoining_margins=None, discard=False, max_lines=None) assert resume_at is None for absolute_box in positioned_boxes: absolute_layout( context, absolute_box, box, positioned_boxes, bottom_space=0, skip_stack=None) vertical_align = box.style['vertical_align'] # Every other value is read as 'top', ie. no change. if vertical_align in ('middle', 'bottom') and box.children: first_child = box.children[0] last_child = box.children[-1] top = first_child.position_y # Not always exact because floating point errors # assert top == box.content_box_y() bottom = last_child.position_y + last_child.margin_height() content_height = bottom - top offset = box.height - content_height if vertical_align == 'middle': offset /= 2 for child in box.children: child.translate(0, offset) return box def page_width_or_height(box, containing_block_size): """Take a :class:`OrientedBox` object and set either width, margin-left and margin-right; or height, margin-top and margin-bottom. "The width and horizontal margins of the page box are then calculated exactly as for a non-replaced block element in normal flow. The height and vertical margins of the page box are calculated analogously (instead of using the block height formulas). In both cases if the values are over-constrained, instead of ignoring any margins, the containing block is resized to coincide with the margin edges of the page box." https://drafts.csswg.org/css-page-3/#page-box-page-rule https://www.w3.org/TR/CSS21/visudet.html#blockwidth """ remaining = containing_block_size - box.padding_plus_border if box.inner == 'auto': if box.margin_a == 'auto': box.margin_a = 0 if box.margin_b == 'auto': box.margin_b = 0 box.inner = remaining - box.margin_a - box.margin_b elif box.margin_a == box.margin_b == 'auto': box.margin_a = box.margin_b = (remaining - box.inner) / 2 elif box.margin_a == 'auto': box.margin_a = remaining - box.inner - box.margin_b elif box.margin_b == 'auto': box.margin_b = remaining - box.inner - box.margin_a box.restore_box_attributes() @handle_min_max_width def page_width(box, context, containing_block_width): page_width_or_height(HorizontalBox(context, box), containing_block_width) @handle_min_max_height def page_height(box, context, containing_block_height): page_width_or_height(VerticalBox(context, box), containing_block_height) def make_page(context, root_box, page_type, resume_at, page_number, page_state): """Take just enough content from the beginning to fill one page. Return ``(page, finished)``. ``page`` is a laid out PageBox object and ``resume_at`` indicates where in the document to start the next page, or is ``None`` if this was the last page. :param int page_number: Page number, starts at 1 for the first page. :param resume_at: As returned by ``make_page()`` for the previous page, or ``None`` for the first page. """ style = context.style_for(page_type) # Propagated from the root or . style['overflow'] = root_box.viewport_overflow page = boxes.PageBox(page_type, style) device_size = page.style['size'] resolve_percentages(page, device_size) page.position_x = 0 page.position_y = 0 cb_width, cb_height = device_size page_width(page, context, cb_width) page_height(page, context, cb_height) root_box.position_x = page.content_box_x() root_box.position_y = page.content_box_y() context.page_bottom = root_box.position_y + page.height initial_containing_block = page footnote_area_style = context.style_for(page_type, '@footnote') footnote_area = boxes.FootnoteAreaBox(page, footnote_area_style) resolve_percentages(footnote_area, page) footnote_area.position_x = page.content_box_x() footnote_area.position_y = context.page_bottom if page_type.blank: previous_resume_at = resume_at root_box = root_box.copy_with_children([]) # TODO: handle cases where the root element is something else. # See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo assert isinstance(root_box, ( boxes.BlockBox, boxes.FlexContainerBox, boxes.GridContainerBox)) context.create_block_formatting_context() context.current_page = page_number context.current_page_footnotes = [] context.current_footnote_area = footnote_area reported_footnotes = context.reported_footnotes context.reported_footnotes = [] for i, reported_footnote in enumerate(reported_footnotes): context.footnotes.append(reported_footnote) overflow = context.layout_footnote(reported_footnote) if overflow and i != 0: context.report_footnote(reported_footnote) context.reported_footnotes = reported_footnotes[i:] break page_is_empty = True adjoining_margins = [] positioned_boxes = [] # Mixed absolute and fixed out_of_flow_boxes = [] broken_out_of_flow = {} context_out_of_flow = context.broken_out_of_flow.values() context.broken_out_of_flow = broken_out_of_flow for box, containing_block, skip_stack in context_out_of_flow: box.position_y = root_box.content_box_y() if box.is_floated(): out_of_flow_box, out_of_flow_resume_at = float_layout( context, box, containing_block, positioned_boxes, positioned_boxes, 0, skip_stack) else: assert box.is_absolutely_positioned() out_of_flow_box, out_of_flow_resume_at = absolute_box_layout( context, box, containing_block, positioned_boxes, 0, skip_stack) out_of_flow_boxes.append(out_of_flow_box) if out_of_flow_resume_at: broken_out_of_flow[out_of_flow_box] = ( box, containing_block, out_of_flow_resume_at) root_box, resume_at, next_page, _, _, _ = block_level_layout( context, root_box, 0, resume_at, initial_containing_block, page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins) assert root_box root_box.children = out_of_flow_boxes + root_box.children footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy()) footnote_area = block_level_layout( context, footnote_area, -inf, None, footnote_area.page, True, positioned_boxes, positioned_boxes)[0] footnote_area.translate(dy=-footnote_area.margin_height()) page.fixed_boxes = [ placeholder._box for placeholder in positioned_boxes if placeholder._box.style['position'] == 'fixed'] for absolute_box in positioned_boxes: absolute_layout( context, absolute_box, page, positioned_boxes, bottom_space=0, skip_stack=None) context.finish_block_formatting_context(root_box) page.children = [root_box, footnote_area] # Update page counter values _standardize_page_based_counters(style, None) build.update_counters(page_state, style) page_counter_values = page_state[1] # page_counter_values will be cached in the page_maker target_collector = context.target_collector page_maker = context.page_maker # remake_state tells the make_all_pages-loop in layout_document() # whether and what to re-make. remake_state = page_maker[page_number - 1][-1] # Evaluate and cache page values only once (for the first LineBox) # otherwise we suffer endless loops when the target/pseudo-element # spans across multiple pages cached_anchors = [] cached_lookups = [] for (_, _, _, _, x_remake_state) in page_maker[:page_number - 1]: cached_anchors.extend(x_remake_state.get('anchors', [])) cached_lookups.extend(x_remake_state.get('content_lookups', [])) for child in page.descendants(placeholders=True): # Cache target's page counters anchor = child.style['anchor'] if anchor and anchor not in cached_anchors: remake_state['anchors'].append(anchor) cached_anchors.append(anchor) # Re-make of affected targeting boxes is inclusive target_collector.cache_target_page_counters( anchor, page_counter_values, page_number - 1, page_maker) # string-set and bookmark-labels don't create boxes, only `content` # requires another call to make_page. There is maximum one 'content' # item per box. if child.missing_link: # A CounterLookupItem exists for the css-token 'content' counter_lookup = target_collector.counter_lookup_items.get( (child.missing_link, 'content')) else: counter_lookup = None # Resolve missing (page based) counters if counter_lookup is not None: call_parse_again = False # Prevent endless loops counter_lookup_id = id(counter_lookup) refresh_missing_counters = counter_lookup_id not in cached_lookups if refresh_missing_counters: remake_state['content_lookups'].append(counter_lookup_id) cached_lookups.append(counter_lookup_id) counter_lookup.page_maker_index = page_number - 1 # Step 1: page based back-references # Marked as pending by target_collector.cache_target_page_counters if counter_lookup.pending: if (page_counter_values != counter_lookup.cached_page_counter_values): counter_lookup.cached_page_counter_values = copy.deepcopy( page_counter_values) counter_lookup.pending = False call_parse_again = True # Step 2: local counters # If the box mixed-in page counters changed, update the content # and cache the new values. missing_counters = counter_lookup.missing_counters if missing_counters: if 'pages' in missing_counters: remake_state['pages_wanted'] = True if refresh_missing_counters and page_counter_values != \ counter_lookup.cached_page_counter_values: counter_lookup.cached_page_counter_values = \ copy.deepcopy(page_counter_values) for counter_name in missing_counters: counter_value = page_counter_values.get( counter_name, None) if counter_value is not None: call_parse_again = True # no need to loop them all break # Step 3: targeted counters target_missing = counter_lookup.missing_target_counters for anchor_name, missed_counters in target_missing.items(): if 'pages' not in missed_counters: continue # Adjust 'pages_wanted' item = target_collector.target_lookup_items.get( anchor_name, None) page_maker_index = item.page_maker_index if page_maker_index >= 0 and anchor_name in cached_anchors: page_maker[page_maker_index][-1]['pages_wanted'] = True # 'content_changed' is triggered in # targets.cache_target_page_counters() if call_parse_again: remake_state['content_changed'] = True counter_lookup.parse_again(page_counter_values) if page_type.blank: resume_at = previous_resume_at next_page = page_maker[page_number - 1][1] return page, resume_at, next_page def set_page_type_computed_styles(page_type, html, style_for): """Set style for page types and pseudo-types matching ``page_type``.""" style_for.add_page_declarations(page_type) # Apply style for page style_for.set_computed_styles( page_type, # @page inherits from the root element: # https://lists.w3.org/Archives/Public/www-style/2012Jan/1164.html root=html.etree_element, parent=html.etree_element, base_url=html.base_url) # Apply style for page pseudo-elements (margin boxes) for element, pseudo_type in style_for.get_cascaded_styles(): if pseudo_type and element == page_type: style_for.set_computed_styles( element, pseudo_type=pseudo_type, # The pseudo-element inherits from the element. root=html.etree_element, parent=element, base_url=html.base_url) def remake_page(index, group_index, context, root_box, html): """Return one laid out page without margin boxes. Start with the initial values from ``context.page_maker[index]``. The resulting values / initial values for the next page are stored in the ``page_maker``. As the function's name suggests: the plan is not to make all pages repeatedly when a missing counter was resolved, but rather re-make the single page where the ``content_changed`` happened. """ page_maker = context.page_maker (initial_resume_at, initial_next_page, right_page, initial_page_state, remake_state) = page_maker[index] # PageType for current page, values for page_maker[index + 1]. # Don't modify actual page_maker[index] values! # TODO: should we store (and reuse) page_type in the page_maker? page_state = copy.deepcopy(initial_page_state) if initial_next_page['break'] in ('left', 'right'): next_page_side = initial_next_page['break'] elif initial_next_page['break'] in ('recto', 'verso'): direction_ltr = root_box.style['direction'] == 'ltr' break_verso = initial_next_page['break'] == 'verso' next_page_side = 'right' if direction_ltr ^ break_verso else 'left' else: next_page_side = None blank = bool( (next_page_side == 'left' and right_page) or (next_page_side == 'right' and not right_page) or (context.reported_footnotes and initial_resume_at is None)) name = '' if blank else initial_next_page['page'] if name not in group_index: group_index.clear() group_index[name] = 0 side = 'right' if right_page else 'left' page_type = PageType(side, blank, name, index, group_index[name]) set_page_type_computed_styles(page_type, html, context.style_for) group_index[name] += 1 context.forced_break = ( initial_next_page['break'] != 'any' or initial_next_page['page']) context.margin_clearance = False # make_page wants a page_number of index + 1 page_number = index + 1 page, resume_at, next_page = make_page( context, root_box, page_type, initial_resume_at, page_number, page_state) assert next_page right_page = not right_page # Check whether we need to append or update the next page_maker item if index + 1 >= len(page_maker): # New page page_maker_next_changed = True else: # Check whether something changed # TODO: Find what we need to compare. Is resume_at enough? (next_resume_at, next_next_page, next_right_page, next_page_state, _) = page_maker[index + 1] page_maker_next_changed = ( next_resume_at != resume_at or next_next_page != next_page or next_right_page != right_page or next_page_state != page_state) if page_maker_next_changed: # Reset remake_state remake_state = { 'content_changed': False, 'pages_wanted': False, 'anchors': [], 'content_lookups': [], } # Setting content_changed to True ensures remake. # If resume_at is None (last page) it must be False to prevent endless # loops and list index out of range (see #794). remake_state['content_changed'] = resume_at is not None # page_state is already a deepcopy item = resume_at, next_page, right_page, page_state, remake_state if index + 1 >= len(page_maker): page_maker.append(item) else: page_maker[index + 1] = item return page, resume_at def make_all_pages(context, root_box, html, pages): """Return a list of laid out pages without margin boxes. Re-make pages only if necessary. """ i = 0 reported_footnotes = None group_i = {'': 0} while True: remake_state = context.page_maker[i][-1] if (len(pages) == 0 or remake_state['content_changed'] or remake_state['pages_wanted']): PROGRESS_LOGGER.info('Step 5 - Creating layout - Page %d', i + 1) # Reset remake_state remake_state['content_changed'] = False remake_state['pages_wanted'] = False remake_state['anchors'] = [] remake_state['content_lookups'] = [] page, resume_at = remake_page(i, group_i, context, root_box, html) reported_footnotes = context.reported_footnotes yield page else: PROGRESS_LOGGER.info( 'Step 5 - Creating layout - Page %d (up-to-date)', i + 1) resume_at = context.page_maker[i + 1][0] reported_footnotes = None yield pages[i] i += 1 if resume_at is None and not reported_footnotes: # Throw away obsolete pages and content context.page_maker = context.page_maker[:i + 1] context.broken_out_of_flow.clear() context.reported_footnotes.clear() return