"""Layout for images and other replaced elements. See https://drafts.csswg.org/css-images-3/#sizing """ from .min_max import handle_min_max_height, handle_min_max_width from .percent import percentage def default_image_sizing(intrinsic_width, intrinsic_height, intrinsic_ratio, specified_width, specified_height, default_width, default_height): """Default sizing algorithm for the concrete object size. Return a ``(concrete_width, concrete_height)`` tuple. See https://drafts.csswg.org/css-images-3/#default-sizing """ if specified_width == 'auto': specified_width = None if specified_height == 'auto': specified_height = None if specified_width is not None and specified_height is not None: return specified_width, specified_height elif specified_width is not None: return specified_width, ( specified_width / intrinsic_ratio if intrinsic_ratio is not None else intrinsic_height if intrinsic_height is not None else default_height) elif specified_height is not None: return ( specified_height * intrinsic_ratio if intrinsic_ratio is not None else intrinsic_width if intrinsic_width is not None else default_width ), specified_height else: if intrinsic_width is not None or intrinsic_height is not None: return default_image_sizing( intrinsic_width, intrinsic_height, intrinsic_ratio, intrinsic_width, intrinsic_height, default_width, default_height) else: return contain_constraint_image_sizing( default_width, default_height, intrinsic_ratio) def contain_constraint_image_sizing(constraint_width, constraint_height, intrinsic_ratio): """Contain constraint sizing algorithm for the concrete object size. Return a ``(concrete_width, concrete_height)`` tuple. See https://drafts.csswg.org/css-images-3/#contain-constraint """ return _constraint_image_sizing( constraint_width, constraint_height, intrinsic_ratio, cover=False) def cover_constraint_image_sizing(constraint_width, constraint_height, intrinsic_ratio): """Cover constraint sizing algorithm for the concrete object size. Return a ``(concrete_width, concrete_height)`` tuple. See https://drafts.csswg.org/css-images-3/#cover-constraint """ return _constraint_image_sizing( constraint_width, constraint_height, intrinsic_ratio, cover=True) def _constraint_image_sizing(constraint_width, constraint_height, intrinsic_ratio, cover): if intrinsic_ratio is None: return constraint_width, constraint_height elif cover ^ (constraint_width > constraint_height * intrinsic_ratio): return constraint_height * intrinsic_ratio, constraint_height else: return constraint_width, constraint_width / intrinsic_ratio def replacedbox_layout(box): # TODO: respect box-sizing ? object_fit = box.style['object_fit'] position = box.style['object_position'] image = box.replacement intrinsic_width, intrinsic_height, intrinsic_ratio = ( image.get_intrinsic_size( box.style['image_resolution'], box.style['font_size'])) if None in (intrinsic_width, intrinsic_height): intrinsic_width, intrinsic_height = contain_constraint_image_sizing( box.width, box.height, intrinsic_ratio) if object_fit == 'fill': draw_width, draw_height = box.width, box.height else: if object_fit in ('contain', 'scale-down'): draw_width, draw_height = contain_constraint_image_sizing( box.width, box.height, intrinsic_ratio) elif object_fit == 'cover': draw_width, draw_height = cover_constraint_image_sizing( box.width, box.height, intrinsic_ratio) else: assert object_fit == 'none', object_fit draw_width, draw_height = intrinsic_width, intrinsic_height if object_fit == 'scale-down': draw_width = min(draw_width, intrinsic_width) draw_height = min(draw_height, intrinsic_height) origin_x, position_x, origin_y, position_y = position[0] ref_x = box.width - draw_width ref_y = box.height - draw_height position_x = percentage(position_x, ref_x) position_y = percentage(position_y, ref_y) if origin_x == 'right': position_x = ref_x - position_x if origin_y == 'bottom': position_y = ref_y - position_y position_x += box.content_box_x() position_y += box.content_box_y() return draw_width, draw_height, position_x, position_y @handle_min_max_width def replaced_box_width(box, containing_block): """Set the used width for replaced boxes.""" from .block import block_level_width width, height, ratio = box.replacement.get_intrinsic_size( box.style['image_resolution'], box.style['font_size']) # This algorithm simply follows the different points of the specification: # https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width if box.height == box.width == 'auto': if width is not None: # Point #1 box.width = width elif ratio is not None: if height is not None: # Point #2 first part box.width = height * ratio else: # Point #3 block_level_width(box, containing_block) if box.width == 'auto': if ratio is not None: # Point #2 second part box.width = box.height * ratio elif width is not None: # Point #4 box.width = width else: # Point #5 # It's pretty useless to rely on device size to set width. box.width = 300 @handle_min_max_height def replaced_box_height(box): """Compute and set the used height for replaced boxes.""" # https://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height width, height, ratio = box.replacement.get_intrinsic_size( box.style['image_resolution'], box.style['font_size']) # Test 'auto' on the computed width, not the used width if box.height == box.width == 'auto': box.height = height elif box.height == 'auto' and ratio: box.height = box.width / ratio if box.height == box.width == 'auto' and height is not None: box.height = height elif ratio is not None and box.height == 'auto': box.height = box.width / ratio elif box.height == 'auto' and height is not None: box.height = height elif box.height == 'auto': # It's pretty useless to rely on device size to set width. box.height = 150 def inline_replaced_box_layout(box, containing_block): """Lay out an inline :class:`boxes.ReplacedBox` ``box``.""" for side in ('top', 'right', 'bottom', 'left'): if getattr(box, f'margin_{side}') == 'auto': setattr(box, f'margin_{side}', 0) inline_replaced_box_width_height(box, containing_block) def inline_replaced_box_width_height(box, containing_block): if box.style['width'] == box.style['height'] == 'auto': replaced_box_width.without_min_max(box, containing_block) replaced_box_height.without_min_max(box) min_max_auto_replaced(box) else: replaced_box_width(box, containing_block) replaced_box_height(box) def min_max_auto_replaced(box): """Resolve min/max constraints on replaced elements with 'auto' sizes.""" width = box.width height = box.height min_width = box.min_width min_height = box.min_height max_width = max(min_width, box.max_width) max_height = max(min_height, box.max_height) # (violation_width, violation_height) violations = ( 'min' if width < min_width else 'max' if width > max_width else '', 'min' if height < min_height else 'max' if height > max_height else '') # Work around divisions by zero. These are pathological cases anyway. # TODO: is there a cleaner way? if width == 0: width = 1e-6 if height == 0: height = 1e-6 # ('', ''): nothing to do if violations == ('max', ''): box.width = max_width box.height = max(max_width * height / width, min_height) elif violations == ('min', ''): box.width = min_width box.height = min(min_width * height / width, max_height) elif violations == ('', 'max'): box.width = max(max_height * width / height, min_width) box.height = max_height elif violations == ('', 'min'): box.width = min(min_height * width / height, max_width) box.height = min_height elif violations == ('max', 'max'): if max_width / width <= max_height / height: box.width = max_width box.height = max(min_height, max_width * height / width) else: box.width = max(min_width, max_height * width / height) box.height = max_height elif violations == ('min', 'min'): if min_width / width <= min_height / height: box.width = min(max_width, min_height * width / height) box.height = min_height else: box.width = min_width box.height = min(max_height, min_width * height / width) elif violations == ('min', 'max'): box.width = min_width box.height = max_height elif violations == ('max', 'min'): box.width = max_width box.height = min_height def block_replaced_box_layout(context, box, containing_block): """Lay out the block :class:`boxes.ReplacedBox` ``box``.""" from .block import block_level_width from .float import avoid_collisions box = box.copy() if box.style['width'] == box.style['height'] == 'auto': computed_margins = box.margin_left, box.margin_right block_replaced_width.without_min_max( box, containing_block) replaced_box_height.without_min_max(box) min_max_auto_replaced(box) box.margin_left, box.margin_right = computed_margins block_level_width.without_min_max(box, containing_block) else: block_replaced_width(box, containing_block) replaced_box_height(box) # Don't collide with floats # https://www.w3.org/TR/CSS21/visuren.html#floats box.position_x, box.position_y, _ = avoid_collisions( context, box, containing_block, outer=False) resume_at = None next_page = {'break': 'any', 'page': None} adjoining_margins = [] collapsing_through = False return box, resume_at, next_page, adjoining_margins, collapsing_through @handle_min_max_width def block_replaced_width(box, containing_block): from .block import block_level_width # https://www.w3.org/TR/CSS21/visudet.html#block-replaced-width replaced_box_width.without_min_max(box, containing_block) block_level_width.without_min_max(box, containing_block)