LOGGER = logging.getLogger('weasyprint') def _dlopen(ffi, *names):
def unicode_to_char_p(string): class Layout:
    """Object holding PangoLayout-related cdata pointers."""
    def __init__(self, context, font_size, style, justification_spacing=0,
                 max_width=None):
        self.justification_spacing = justification_spacing
        self.setup(context, font_size, style)
        self.max_width = max_width

    def setup(self, context, font_size, style):
        self.context = context
        self.style = style
        self.first_line_direction = 0

        if context is None:
            font_map = ffi.gc(
                pangoft2.pango_ft2_font_map_new(), gobject.g_object_unref)
        else:
            font_map = context.font_config.font_map
        pango_context = ffi.gc(
            pango.pango_font_map_create_context(font_map),
            gobject.g_object_unref)
        pango.pango_context_set_round_glyph_positions(pango_context, False)
        self.layout = ffi.gc(
            pango.pango_layout_new(pango_context),
            gobject.g_object_unref)

        if style['font_language_override'] != 'normal':
            lang_p, lang = unicode_to_char_p(LST_TO_ISO.get(
                style['font_language_override'].lower(),
                style['font_language_override']))
        elif style['lang']:
            lang_p, lang = unicode_to_char_p(style['lang'])
        else:
            lang = None
            self.language = pango.pango_language_get_default()
        if lang:
            self.language = pango.pango_language_from_string(lang_p)
            pango.pango_context_set_language(pango_context, self.language)

        assert not isinstance(style['font_family'], str), (
            'font_family should be a list')
        self.font = ffi.gc(
            pango.pango_font_description_new(),
            pango.pango_font_description_free)
        family_p, family = unicode_to_char_p(','.join(style['font_family']))
        pango.pango_font_description_set_family(self.font, family_p)
        pango.pango_font_description_set_style(
            self.font, PANGO_STYLE[style['font_style']])
        pango.pango_font_description_set_stretch(
            self.font, PANGO_STRETCH[style['font_stretch']])
        pango.pango_font_description_set_weight(
            self.font, style['font_weight'])
        pango.pango_font_description_set_absolute_size(
            self.font, units_from_double(font_size))
        pango.pango_layout_set_font_description(self.layout, self.font)

        text_decoration = style['text_decoration_line']
        if text_decoration != 'none':
            metrics = ffi.gc(
                pango.pango_context_get_metrics(
                    pango_context, self.font, self.language),
                pango.pango_font_metrics_unref)
            self.ascent = units_to_double(
                pango.pango_font_metrics_get_ascent(metrics))
            self.underline_position = units_to_double(
                pango.pango_font_metrics_get_underline_position(metrics))
            self.strikethrough_position = units_to_double(
                pango.pango_font_metrics_get_strikethrough_position(metrics))
            self.underline_thickness = units_to_double(
                pango.pango_font_metrics_get_underline_thickness(metrics))
            self.strikethrough_thickness = units_to_double(
                pango.pango_font_metrics_get_strikethrough_thickness(metrics))
        else:
            self.ascent = None
            self.underline_position = None
            self.strikethrough_position = None

        features = font_features(
            style['font_kerning'], style['font_variant_ligatures'],
            style['font_variant_position'], style['font_variant_caps'],
            style['font_variant_numeric'], style['font_variant_alternates'],
            style['font_variant_east_asian'], style['font_feature_settings'])
        if features and context:
            features = ','.join(
                f'{key} {value}' for key, value in features.items()).encode()
            # TODO: attributes should be freed.
            # In the meantime, keep a cache to avoid leaking too many of them.
            attr = context.font_features.setdefault(
                features, pango.pango_attr_font_features_new(features))
            attr_list = pango.pango_attr_list_new()
            pango.pango_attr_list_insert(attr_list, attr)
            pango.pango_layout_set_attributes(self.layout, attr_list)

    def get_first_line(self):
        first_line = pango.pango_layout_get_line_readonly(self.layout, 0)
        second_line = pango.pango_layout_get_line_readonly(self.layout, 1)
        if second_line != ffi.NULL:
            index = second_line.start_index
        else:
            index = None
        self.first_line_direction = first_line.resolved_dir
        return first_line, index

    def set_text(self, text, justify=False):
        try:
            # Keep only the first line plus one character, we don't need more
            text = text[:text.index('\n') + 2]
        except ValueError:
            # End-of-line not found, keep the whole text
            pass
        text, bytestring = unicode_to_char_p(text)
        self.text = bytestring.decode()
        pango.pango_layout_set_text(self.layout, text, -1)

        word_spacing = self.style['word_spacing']
        if justify:
            # Justification is needed when drawing text but is useless during
            # layout, when it can be ignored.
            word_spacing += self.justification_spacing

        letter_spacing = self.style['letter_spacing']
        if letter_spacing == 'normal':
            letter_spacing = 0

        word_breaking = (
            self.style['overflow_wrap'] in ('anywhere', 'break-word'))

        if self.text and (word_spacing or letter_spacing or word_breaking):
            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
                attr = pango.pango_attr_letter_spacing_new(spacing)
                attr.start_index, attr.end_index = start, end
                pango.pango_attr_list_change(attr_list, attr)

            if letter_spacing:
                letter_spacing = units_from_double(letter_spacing)
                add_attr(0, len(bytestring), letter_spacing)

            if word_spacing:
                space_spacing = (
                    units_from_double(word_spacing) + letter_spacing)
                position = bytestring.find(b' ')
                while position != -1:
                    add_attr(position, position + 1, space_spacing)
                    position = bytestring.find(b' ', position + 1)

            if word_breaking:
                # TODO: attributes should be freed
                attr = pango.pango_attr_insert_hyphens_new(False)
                attr.start_index, attr.end_index = 0, len(bytestring)
                pango.pango_attr_list_change(attr_list, attr)

            pango.pango_layout_set_attributes(self.layout, attr_list)

        # Tabs width
        if b'\t' in bytestring:
            self.set_tabs()

    def set_tabs(self):
        if isinstance(self.style['tab_size'], int):
            layout = Layout(
                self.context, self.style['font_size'], self.style,
                self.justification_spacing)
            layout.set_text(' ' * self.style['tab_size'])
            line, _ = layout.get_first_line()
            width, _ = line_size(line, self.style)
            width = int(round(width))
        else:
            width = int(self.style['tab_size'].value)
        # 0 is not handled correctly by Pango
        array = ffi.gc(
            pango.pango_tab_array_new_with_positions(
                1, True, pango.PANGO_TAB_LEFT, width or 1),
            pango.pango_tab_array_free)
        pango.pango_layout_set_tabs(self.layout, array)

    def deactivate(self):
        del self.layout, self.font, self.language, self.style

    def reactivate(self, style):
        self.setup(self.context, style['font_size'], style)
        self.set_text(self.text, justify=True) def first_line_metrics(first_line, text, layout, resume_at, space_collapse,
                       style, hyphenated=False, hyphenation_character=None):
    length = first_line.length
    if hyphenated:
        length -= len(hyphenation_character.encode())
    elif resume_at:
        # Set an infinite width as we don't want to break lines when drawing,
        # the lines have already been split and the size may differ. Rendering
        # is also much faster when no width is set.
        pango.pango_layout_set_width(layout.layout, -1)

        # Create layout with final text
        first_line_text = text.encode()[:length].decode()

        # Remove trailing spaces if spaces collapse
        if space_collapse:
            first_line_text = first_line_text.rstrip(' ')

        layout.set_text(first_line_text)
        first_line, _ = layout.get_first_line()
        length = first_line.length if first_line is not None else 0

    width, height = line_size(first_line, style)
    baseline = units_to_double(pango.pango_layout_get_baseline(layout.layout))
    layout.deactivate()
    return layout, length, resume_at, width, height, baseline def line_size(line, style):
    """Get logical width and height of the given ``line``.

    ``style`` is used to add letter spacing (if needed).

    """
    logical_extents = ffi.new('PangoRectangle *')
    pango.pango_layout_line_get_extents(line, ffi.NULL, logical_extents)
    width = units_to_double(logical_extents.width)
    height = units_to_double(logical_extents.height)
    ffi.release(logical_extents)
    if style['letter_spacing'] != 'normal':
        width += style['letter_spacing']
    return width, height def get_link_attribute(element, attr_name, base_url):
    """Get the URL value of an element attribute.

    Return ``('external', absolute_uri)``, or ``('internal',
    unquoted_fragment_id)``, or ``None``.

    """
    attr_value = element.get(attr_name, '').strip()
    if attr_value.startswith('#') and len(attr_value) > 1:
        # Do not require a base_url when the value is just a fragment.
        return ('url', ('internal', unquote(attr_value[1:])))
    uri = get_url_attribute(element, attr_name, base_url, allow_relative=True)
    if uri:
        if base_url:
            parsed = urlsplit(uri)
            # Compare with fragments removed
            if parsed[:-1] == urlsplit(base_url)[:-1]:
                return ('url', ('internal', unquote(parsed.fragment)))
        return ('url', ('external', uri)) INITIAL_VALUES = {
    # CSS 2.1: http://www.w3.org/TR/CSS21/propidx.html
    'bottom': 'auto',
    'caption_side': 'top',
    'clear': 'none',
    'clip': (),  # computed value for 'auto'
    'color': parse_color('black'),  # chosen by the user agent
    'direction': 'ltr',
    'display': ('inline', 'flow'),
    'empty_cells': 'show',
    'float': 'none',
    'height': 'auto',
    'left': 'auto',
    'line_height': 'normal',
    'margin_top': Dimension(0, 'px'),
    'margin_right': Dimension(0, 'px'),
    'margin_bottom': Dimension(0, 'px'),
    'margin_left': Dimension(0, 'px'),
    'max_height': Dimension(inf, 'px'),  # parsed value for 'none'
    'max_width': Dimension(inf, 'px'),
    'padding_top': Dimension(0, 'px'),
    'padding_right': Dimension(0, 'px'),
    'padding_bottom': Dimension(0, 'px'),
    'padding_left': Dimension(0, 'px'),
    'position': 'static',
    'right': 'auto',
    'table_layout': 'auto',
    'top': 'auto',
    'unicode_bidi': 'normal',
    'vertical_align': 'baseline',
    'visibility': 'visible',
    'width': 'auto',
    'z_index': 'auto',

    # Backgrounds and Borders 3 (CR): https://www.w3.org/TR/css3-background/
    'background_attachment': ('scroll',),
    'background_clip': ('border-box',),
    'background_color': parse_color('transparent'),
    'background_image': (('none', None),),
    'background_origin': ('padding-box',),
    'background_position': (('left', Dimension(0, '%'),
                             'top', Dimension(0, '%')),),
    'background_repeat': (('repeat', 'repeat'),),
    'background_size': (('auto', 'auto'),),
    'border_bottom_color': 'currentColor',
    'border_bottom_left_radius': (Dimension(0, 'px'), Dimension(0, 'px')),
    'border_bottom_right_radius': (Dimension(0, 'px'), Dimension(0, 'px')),
    'border_bottom_style': 'none',
    'border_bottom_width': 3,
    'border_collapse': 'separate',
    'border_left_color': 'currentColor',
    'border_left_style': 'none',
    'border_left_width': 3,
    'border_right_color': 'currentColor',
    'border_right_style': 'none',
    'border_right_width': 3,
    'border_spacing': (0, 0),
    'border_top_color': 'currentColor',
    'border_top_left_radius': (Dimension(0, 'px'), Dimension(0, 'px')),
    'border_top_right_radius': (Dimension(0, 'px'), Dimension(0, 'px')),
    'border_top_style': 'none',
    'border_top_width': 3,  # computed value for 'medium'

    # Color 3 (REC): https://www.w3.org/TR/css3-color/
    'opacity': 1,

    # Multi-column Layout (WD): https://www.w3.org/TR/css-multicol-1/
    'column_width': 'auto',
    'column_count': 'auto',
    'column_gap': Dimension(1, 'em'),
    'column_rule_color': 'currentColor',
    'column_rule_style': 'none',
    'column_rule_width': 'medium',
    'column_fill': 'balance',
    'column_span': 'none',

    # Fonts 3 (REC): https://www.w3.org/TR/css-fonts-3/
    'font_family': ('serif',),  # depends on user agent
    'font_feature_settings': 'normal',
    'font_kerning': 'auto',
    'font_language_override': 'normal',
    'font_size': 16,  # actually medium, but we define medium from this
    'font_stretch': 'normal',
    'font_style': 'normal',
    'font_variant': 'normal',
    'font_variant_alternates': 'normal',
    'font_variant_caps': 'normal',
    'font_variant_east_asian': 'normal',
    'font_variant_ligatures': 'normal',
    'font_variant_numeric': 'normal',
    'font_variant_position': 'normal',
    'font_weight': 400,

    # Fragmentation 3/4 (CR/WD): https://www.w3.org/TR/css-break-4/
    'box_decoration_break': 'slice',
    'break_after': 'auto',
    'break_before': 'auto',
    'break_inside': 'auto',
    'margin_break': 'auto',
    'orphans': 2,
    'widows': 2,

    # Generated Content 3 (WD): https://www.w3.org/TR/css-content-3/
    'bookmark_label': (('content', 'text'),),
    'bookmark_level': 'none',
    'bookmark_state': 'open',
    'content': 'normal',
    'footnote_display': 'block',
    'footnote_policy': 'auto',
    'quotes': list('“”‘’'),  # chosen by the user agent
    'string_set': 'none',

    # Images 3/4 (CR/WD): https://www.w3.org/TR/css4-images/
    'image_resolution': 1,  # dppx
    'image_rendering': 'auto',
    # https://drafts.csswg.org/css-images-3/
    'object_fit': 'fill',
    'object_position': (('left', Dimension(50, '%'),
                         'top', Dimension(50, '%')),),

    # Paged Media 3 (WD): https://www.w3.org/TR/css-page-3/
    'size': None,  # set to A4 in computed_values
    'page': 'auto',
    'bleed_left': 'auto',
    'bleed_right': 'auto',
    'bleed_top': 'auto',
    'bleed_bottom': 'auto',
    'marks': (),  # computed value for 'none'

    # Text 3/4 (WD/WD): https://www.w3.org/TR/css-text-4/
    'hyphenate_character': '‐',  # computed value chosen by the user agent
    'hyphenate_limit_chars': (5, 2, 2),
    'hyphenate_limit_zone': Dimension(0, 'px'),
    'hyphens': 'manual',
    'letter_spacing': 'normal',
    'tab_size': 8,
    'text_align_all': 'start',
    'text_align_last': 'auto',
    'text_indent': Dimension(0, 'px'),
    'text_transform': 'none',
    'white_space': 'normal',
    'word_break': 'normal',
    'word_spacing': 0,  # computed value for 'normal'

    # Transforms 1 (CR): https://www.w3.org/TR/css-transforms-1/
    'transform_origin': (Dimension(50, '%'), Dimension(50, '%')),
    'transform': (),  # computed value for 'none'

    # User Interface 3 (REC): https://www.w3.org/TR/css-ui-3/
    'box_sizing': 'content-box',
    'outline_color': 'currentColor',  # invert is not supported
    'outline_style': 'none',
    'outline_width': 3,  # computed value for 'medium'
    'overflow_wrap': 'normal',

    # Flexible Box Layout Module 1 (CR): https://www.w3.org/TR/css-flexbox-1/
    'align_content': 'stretch',
    'align_items': 'stretch',
    'align_self': 'auto',
    'flex_basis': 'auto',
    'flex_direction': 'row',
    'flex_grow': 0,
    'flex_shrink': 1,
    'flex_wrap': 'nowrap',
    'justify_content': 'flex-start',
    'min_height': 'auto',
    'min_width': 'auto',
    'order': 0,

    # Text Decoration Module 3 (CR): https://www.w3.org/TR/css-text-decor-3/
    'text_decoration_line': 'none',
    'text_decoration_color': 'currentColor',
    'text_decoration_style': 'solid',

    # Overflow Module 3 (WD): https://www.w3.org/TR/css-overflow-3/
    'block_ellipsis': 'none',
    'continue': 'auto',
    'max_lines': 'none',
    'overflow': 'visible',
    'text_overflow': 'clip',

    # Lists Module 3 (WD): https://drafts.csswg.org/css-lists-3/
    # Means 'none', but allow `display: list-item` to increment the
    # list-item counter. If we ever have a way for authors to query
    # computed values (JavaScript?), this value should serialize to 'none'.
    'counter_increment': 'auto',
    'counter_reset': (),  # parsed value for 'none'
    'counter_set': (),  # parsed value for 'none'
    'list_style_image': ('none', None),
    'list_style_position': 'outside',
    'list_style_type': 'disc',

    # Proprietary
    'anchor': None,  # computed value of 'none'
    'link': None,  # computed value of 'none'
    'lang': None,  # computed value of 'none'
}
KNOWN_PROPERTIES = set(name.replace('_', '-') for name in INITIAL_VALUES)
INHERITED = {
    'block_ellipsis',
    'border_collapse',
    'border_spacing',
    'caption_side',
    'color',
    'direction',
    'empty_cells',
    'font_family',
    'font_feature_settings',
    'font_kerning',
    'font_language_override',
    'font_size',
    'font_style',
    'font_stretch',
    'font_variant',
    'font_variant_alternates',
    'font_variant_caps',
    'font_variant_east_asian',
    'font_variant_ligatures',
    'font_variant_numeric',
    'font_variant_position',
    'font_weight',
    'hyphens',
    'hyphenate_character',
    'hyphenate_limit_chars',
    'hyphenate_limit_zone',
    'image_rendering',
    'image_resolution',
    'lang',
    'letter_spacing',
    'line_height',
    'link',
    'list_style_image',
    'list_style_position',
    'list_style_type',
    'orphans',
    'overflow_wrap',
    'quotes',
    'tab_size',
    'text_align_all',
    'text_align_last',
    'text_decoration_line',
    'text_decoration_color',
    'text_decoration_style',
    'text_indent',
    'text_transform',
    'visibility',
    'white_space',
    'widows',
    'word_break',
    'word_spacing',
}
TABLE_WRAPPER_BOX_PROPERTIES = {
    'bottom',
    'break_after',
    'break_before',
    'break_inside',
    'clear',
    'counter_increment',
    'counter_reset',
    'counter_set',
    'float',
    'left',
    'margin_top',
    'margin_bottom',
    'margin_left',
    'margin_right',
    'opacity',
    'overflow',
    'position',
    'right',
    'top',
    'transform',
    'transform_origin',
    'vertical_align',
    'z_index',
}
INITIAL_NOT_COMPUTED = {
    'display',
    'column_gap',
    'bleed_top',
    'bleed_left',
    'bleed_bottom',
    'bleed_right',
    'outline_width',
    'outline_color',
    'column_rule_width',
    'column_rule_color',
    'border_top_width',
    'border_left_width',
    'border_bottom_width',
    'border_right_width',
    'border_top_color',
    'border_left_color',
    'border_bottom_color',
    'border_right_color',
} ANGLE_TO_RADIANS = {
    'rad': 1,
    'turn': 2 * math.pi,
    'deg': math.pi / 180,
    'grad': math.pi / 200,
} LENGTH_UNITS = set(LENGTHS_TO_PIXELS) | set(['ex', 'em', 'ch', 'rem']) LENGTHS_TO_PIXELS = {
    'px': 1,
    'pt': 1. / 0.75,
    'pc': 16.,  # LENGTHS_TO_PIXELS['pt'] * 12
    'in': 96.,  # LENGTHS_TO_PIXELS['pt'] * 72
    'cm': 96. / 2.54,  # LENGTHS_TO_PIXELS['in'] / 2.54
    'mm': 96. / 25.4,  # LENGTHS_TO_PIXELS['in'] / 25.4
    'q': 96. / 25.4 / 4,  # LENGTHS_TO_PIXELS['mm'] / 4
} def check_var_function(token):
    function = parse_function(token)
    if function is None:
        return
    name, args = function
    if name == 'var' and args:
        ident = args.pop(0)
        if ident.type != 'ident' or not ident.value.startswith('--'):
            return

        # TODO: we should check authorized tokens
        # https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
        return ('var()', (ident.value.replace('-', '_'), args or None)) def safe_urljoin(base_url, url):
    if url_is_absolute(url):
        return iri_to_uri(url)
    elif base_url:
        return iri_to_uri(urljoin(base_url, url))
    else:
        raise InvalidValues(
            f'Relative URI reference without a base URI: {url!r}') PROPERTIES = {}
from collections import OrderedDict
from urllib.parse import unquote
from tinycss2.color3 import parse_color
from ..logger import LOGGER
from ..text.ffi import ffi, pango, units_to_double
from ..text.line_break import Layout, first_line_metrics, line_size
from ..urls import get_link_attribute
from .properties import (
    INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, Dimension)
from .utils import (
    ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, check_var_function,
    safe_urljoin)
from .validation.properties import PROPERTIES


@register_computer('line-height')
def line_height(style, name, value):
    """Compute the ``line-height`` property."""
    if value == 'normal':
        return value
    elif not value.unit:
        return ('NUMBER', value.value)
    elif value.unit == '%':
        factor = value.value / 100
        font_size_value = style['font_size']
        pixels = factor * font_size_value
    else:
        pixels = length(style, name, value, pixels_only=True)
    return ('PIXELS', pixels)


@register_computer('anchor')
def anchor(style, name, values):
    """Compute the ``anchor`` property."""
    if values != 'none':
        _, key = values
        anchor_name = style.element.get(key) or None
        return anchor_name


@register_computer('link')
def link(style, name, values):
    """Compute the ``link`` property."""
    if values == 'none':
        return None
    else:
        type_, value = values
        if type_ == 'attr()':
            return get_link_attribute(style.element, value, style.base_url)
        else:
            return values


@register_computer('lang')
def lang(style, name, values):
    """Compute the ``lang`` property."""
    if values == 'none':
        return None
    else:
        type_, key = values
        if type_ == 'attr()':
            return style.element.get(key) or None
        elif type_ == 'string':
            return key


@register_computer('tab-size')
def tab_size(style, name, value):
    """Compute the ``tab-size`` property."""
    if isinstance(value, int):
        return value
    else:
        return length(style, name, value)


@register_computer('transform')
def transform(style, name, value):
    """Compute the ``transform`` property."""
    result = []
    for function, args in value:
        if function == 'translate':
            args = length_or_percentage_tuple(style, name, args)
        result.append((function, args))
    return tuple(result)


@register_computer('vertical-align')
def vertical_align(style, name, value):
    """Compute the ``vertical-align`` property."""
    # Use +/- half an em for super and sub, same as Pango.
    # (See the SUPERSUB_RISE constant in pango-markup.c)
    if value in ('baseline', 'middle', 'text-top', 'text-bottom',
                 'top', 'bottom'):
        return value
    elif value == 'super':
        return style['font_size'] * 0.5
    elif value == 'sub':
        return style['font_size'] * -0.5
    elif value.unit == '%':
        height, _ = strut_layout(style)
        return height * value.value / 100
    else:
        return length(style, name, value, pixels_only=True)


@register_computer('word-spacing')
def word_spacing(style, name, value):
    """Compute the ``word-spacing`` property."""
    if value == 'normal':
        return 0
    else:
        return length(style, name, value, pixels_only=True)


def strut_layout(style, context=None):
    """Return a tuple of the used value of ``line-height`` and the baseline.

    The baseline is given from the top edge of line height.

    """
    # TODO: always get the real value for `context`? (if we really care…)

    if style['font_size'] == 0:
        return 0, 0

    if context:
        key = (
            style['font_size'], style['font_language_override'], style['lang'],
            tuple(style['font_family']), style['font_style'],
            style['font_stretch'], style['font_weight'], style['line_height'])
        if key in context.strut_layouts:
            return context.strut_layouts[key]


    layout = Layout(context, style['font_size'], style)