    def draw_node(self, node, font_size, fill_stroke=True):
        """Draw a node."""
        if node.tag == 'defs':
            return

        # Update font size
        font_size = size(node.get('font-size', '1em'), font_size, font_size)

        original_streams = []

        if fill_stroke:
            self.stream.push_state()

        # Apply filters
        filter_ = self.filters.get(parse_url(node.get('filter')).fragment)
        if filter_:
            apply_filters(self, node, filter_, font_size)

        # Apply transform attribute
        self.transform(node.get('transform'), font_size)

        # Create substream for opacity
        opacity = alpha_value(node.get('opacity', 1))
        if fill_stroke and 0 <= opacity < 1:
            original_streams.append(self.stream)
            box = self.calculate_bounding_box(node, font_size)
            if not is_valid_bounding_box(box):
                box = (0, 0, self.inner_width, self.inner_height)
            self.stream = self.stream.add_group(*box)

        # Clip
        clip_path = parse_url(node.get('clip-path')).fragment
        if clip_path and clip_path in self.paths:
            old_ctm = self.stream.ctm
            clip_path = self.paths[clip_path]
            if clip_path.get('clipPathUnits') == 'objectBoundingBox':
                x, y = self.point(node.get('x'), node.get('y'), font_size)
                width, height = self.point(
                    node.get('width'), node.get('height'), font_size)
                self.stream.transform(a=width, d=height, e=x, f=y)
            original_tag = clip_path._etree_node.tag
            clip_path._etree_node.tag = 'g'
            self.draw_node(clip_path, font_size, fill_stroke=False)
            clip_path._etree_node.tag = original_tag
            # At least set the clipping area to an empty path, so that it’s
            # totally clipped when the clipping path is empty.
            self.stream.rectangle(0, 0, 0, 0)
            self.stream.clip()
            self.stream.end()
            new_ctm = self.stream.ctm
            if new_ctm.determinant:
                self.stream.transform(*(old_ctm @ new_ctm.invert).values)

        # Handle text anchor and set text bounding box
        text_anchor_shift = False
        if node.display and TAGS.get(node.tag) == text:
            if (text_anchor := node.get('text-anchor')) in ('middle', 'end'):
                text_anchor_shift = True
                group = self.stream.add_group(0, 0, 0, 0)  # BBox set after drawing
                original_streams.append(self.stream)
                self.stream = group
            node.text_bounding_box = EMPTY_BOUNDING_BOX

        # Save concrete size of root svg tag
        if node.tag == 'svg':
            concrete_width = self.concrete_width
            concrete_height = self.concrete_height

        # Draw node
        if node.visible and node.tag in TAGS:
            with suppress(PointError):
                TAGS[node.tag](self, node, font_size)

        # Draw node children
        if node.display and node.tag not in DEF_TYPES:
            for child in node:
                new_chunk = text_anchor_shift and (
                    child.tag == 'text' or 'x' in child.attrib or 'y' in child.attrib)
                if new_chunk:
                    new_stream = self.stream
                    self.stream = original_streams[-1]
                self.draw_node(child, font_size, fill_stroke)
                if new_chunk:
                    self.stream = new_stream
                visible_text_child = (
                    TAGS.get(node.tag) == text and
                    TAGS.get(child.tag) == text and
                    child.visible)
                if visible_text_child:
                    if not is_valid_bounding_box(child.text_bounding_box):
                        continue
                    x1, y1 = child.text_bounding_box[:2]
                    x2 = x1 + child.text_bounding_box[2]
                    y2 = y1 + child.text_bounding_box[3]
                    node.text_bounding_box = extend_bounding_box(
                        node.text_bounding_box, ((x1, y1), (x2, y2)))

        # Restore concrete and inner size of root svg tag
        if node.tag == 'svg':
            self.tree.set_svg_size(svg, concrete_width, concrete_height)

        # Handle text anchor
        if text_anchor_shift:
            group_id = self.stream.id
            self.stream = original_streams.pop()
            self.stream.push_state()
            if is_valid_bounding_box(node.text_bounding_box):
                x, y, width, height = node.text_bounding_box
                # Add extra space to include ink extents
                group.extra['BBox'][:] = (
                    x - font_size, y - font_size,
                    x + width + font_size, y + height + font_size)
                x_align = width / 2 if text_anchor == 'middle' else width
                if node.tag == 'text' or 'x' in node.attrib or 'y' in node.attrib:
                    self.stream.transform(e=-x_align)
            self.stream.draw_x_object(group_id)
            self.stream.pop_state()

        # Apply mask
        mask = self.masks.get(parse_url(node.get('mask')).fragment)
        if mask:
            paint_mask(self, node, mask, opacity)

        # Fill and stroke
        if fill_stroke:
            self.fill_stroke(node, font_size)

        # Draw markers
        self.draw_markers(node, font_size, fill_stroke)

        # Apply opacity stream and restore original stream
        if fill_stroke and 0 <= opacity < 1:
            group_id = self.stream.id
            self.stream = original_streams.pop()
            self.stream.set_alpha(opacity, stroke=True, fill=True)
            self.stream.draw_x_object(group_id)

        # Clean text tag
        if node.tag == 'text':
            self.cursor_position = [0, 0]
            self.cursor_d_position = [0, 0]
            self.text_path_width = 0

        if fill_stroke:
            self.stream.pop_state()
Draw a node.
weasyprint/svg/__init__.py