#!/usr/bin/env python3 """ PPT Master - Text Outline Materialization Resolve one supported SVG ```` operand into closed glyph-outline path commands for the Shape Boolean geometry pipeline. Usage: Import text_element_to_path_commands from svg_to_pptx.text_outline. Dependencies: uharfbuzz and local PPT Master modules """ from __future__ import annotations import bisect import math import os import sys import unicodedata from collections.abc import Mapping, Sequence from dataclasses import dataclass from functools import lru_cache from pathlib import Path from typing import Any from xml.etree import ElementTree as ET from .drawingml.paths import PathCommand, normalize_path_commands from .drawingml.text_properties import ( normalize_project_text_segments, parse_project_font_style, parse_project_font_weight, parse_project_letter_spacing, parse_project_text_anchor, parse_project_text_decoration, resolve_project_xml_space, ) from .drawingml.utils import ( font_px_to_hpt, parse_inline_style, parse_project_geometry_length, parse_svg_length, split_project_text_clusters, ) _FONT_EXTENSIONS = frozenset({".otc", ".otf", ".ttc", ".ttf"}) _FAMILY_NAME_IDS = (16, 1, 21) _GENERIC_FAMILIES = { "sans-serif": ( "Segoe UI", "Arial", "Helvetica", "Noto Sans", "Liberation Sans", "DejaVu Sans", "Nimbus Sans", "Noto Sans CJK SC", "Source Han Sans SC", "Droid Sans Fallback", ), "system-ui": ( "Segoe UI", "SF Pro", "Helvetica Neue", "Noto Sans", "Liberation Sans", "DejaVu Sans", "Noto Sans CJK SC", "Droid Sans Fallback", ), "serif": ( "Times New Roman", "Noto Serif", "Liberation Serif", "DejaVu Serif", "Nimbus Roman", "Noto Serif CJK SC", "Source Han Serif SC", ), "monospace": ( "Consolas", "Courier New", "Noto Sans Mono", "Liberation Mono", "DejaVu Sans Mono", "Nimbus Mono PS", "Noto Sans Mono CJK SC", ), } _GENERIC_FAMILIES["ui-sans-serif"] = _GENERIC_FAMILIES["system-ui"] _GENERIC_FAMILIES["ui-serif"] = _GENERIC_FAMILIES["serif"] _GENERIC_FAMILIES["ui-monospace"] = _GENERIC_FAMILIES["monospace"] _UNSUPPORTED_TEXT_PROPERTIES = frozenset({ "alignment-baseline", "baseline-shift", "direction", "dominant-baseline", "font-feature-settings", "font-kerning", "font-size-adjust", "font-stretch", "font-synthesis", "font-variant", "font-variation-settings", "font", "hyphens", "kerning", "line-height", "overflow-wrap", "text-align", "text-align-last", "text-indent", "text-orientation", "text-rendering", "text-transform", "unicode-bidi", "vertical-align", "white-space", "word-spacing", "word-break", "writing-mode", }) _UNSUPPORTED_TEXT_ATTRIBUTES = frozenset({ "dx", "dy", "lengthAdjust", "rotate", "textLength", }) _UNSUPPORTED_BIDI_CLASSES = frozenset({ "AL", "FSI", "LRE", "LRI", "LRO", "PDF", "PDI", "R", "RLE", "RLI", "RLO", }) @dataclass(frozen=True) class _FontFace: path: Path index: int families: tuple[str, ...] weight: int italic: bool weight_range: tuple[float, float] | None italic_range: tuple[float, float] | None coverage: frozenset[int] order: int @dataclass(frozen=True) class _TextStyle: families: tuple[str, ...] font_size: float font_weight: int italic: bool anchor: str letter_spacing: float @dataclass(frozen=True) class _FontRun: face: _FontFace clusters: tuple[str, ...] global_cluster_start: int @property def text(self) -> str: return "".join(self.clusters) @dataclass class _ShapedRun: run: _FontRun font: Any infos: Sequence[Any] positions: Sequence[Any] scale: float advance_x: float @dataclass class _DrawState: origin_x: float origin_y: float scale: float commands: list[PathCommand] segment_count: int = 0 def point(self, x: float, y: float) -> tuple[float, float]: return ( self.origin_x + x * self.scale, self.origin_y - y * self.scale, ) def text_element_to_path_commands( element: ET.Element, parents: Mapping[ET.Element, ET.Element], *, font_dirs: Sequence[str | Path] = (), ) -> list[PathCommand]: """Return closed glyph outlines for one supported SVG ```` element.""" chain = _element_chain(element, parents) _validate_text_structure(element, chain) text = _normalized_direct_text(element, chain) if not text: raise ValueError(f"{_element_label(element)} has no rendered text") if any( unicodedata.bidirectional(character) in _UNSUPPORTED_BIDI_CLASSES for character in text ): raise ValueError( f"{_element_label(element)} uses right-to-left or bidirectional " "text; materialize its glyph outlines before Shape Boolean" ) style = _resolve_text_style(chain) x = _text_position(element, "x") y = _text_position(element, "y") explicit_roots = _normalize_explicit_font_dirs(font_dirs) catalog = _font_catalog(explicit_roots) candidates = _font_candidates( catalog, style.families, style.font_weight, style.italic, ) if not candidates: family_list = ", ".join(style.families) raise ValueError( f"{_element_label(element)} cannot resolve an installed font from " f"{family_list!r}; add a usable fallback or pass --font-dir" ) clusters = split_project_text_clusters(text) runs = _font_runs( element, clusters, candidates, style.families, ) shaped_runs = _shape_runs( runs, style.font_size, style.font_weight, style.italic, style.letter_spacing, ) natural_width = sum(run.advance_x for run in shaped_runs) tracked_width = ( natural_width + max(0, len(clusters) - 1) * style.letter_spacing ) if not math.isfinite(tracked_width) or tracked_width <= 0: raise ValueError( f"{_element_label(element)} resolves to a non-positive text width" ) anchor_shift = { "start": 0.0, "middle": -tracked_width / 2.0, "end": -tracked_width, }[style.anchor] hb = _load_harfbuzz() draw_funcs = _draw_functions(hb) commands: list[PathCommand] = [] natural_run_x = 0.0 for shaped in shaped_runs: run_offsets = _cluster_offsets(shaped.run.clusters) glyph_cursor_x = 0.0 glyph_cursor_y = 0.0 for info, position in zip(shaped.infos, shaped.positions): if info.codepoint == 0: raise ValueError( f"{_element_label(element)} shaped a missing glyph in " f"{shaped.run.face.path.name}" ) cluster_index = max( 0, bisect.bisect_right(run_offsets, info.cluster) - 1, ) cluster_index = min( cluster_index, len(shaped.run.clusters) - 1, ) global_cluster = ( shaped.run.global_cluster_start + cluster_index ) spacing_shift = global_cluster * style.letter_spacing state = _DrawState( origin_x=( x + anchor_shift + natural_run_x + glyph_cursor_x + position.x_offset * shaped.scale + spacing_shift ), origin_y=( y - glyph_cursor_y - position.y_offset * shaped.scale ), scale=shaped.scale, commands=commands, ) shaped.font.draw_glyph(info.codepoint, draw_funcs, state) if state.segment_count == 0: cluster = shaped.run.clusters[cluster_index] if not _cluster_may_have_no_outline(cluster): codepoints = " ".join( f"U+{ord(character):04X}" for character in cluster ) raise ValueError( f"{_element_label(element)} glyph for {codepoints} has " "no vector outline; bitmap/color-only glyphs cannot be " "used in Shape Boolean" ) glyph_cursor_x += position.x_advance * shaped.scale glyph_cursor_y += position.y_advance * shaped.scale natural_run_x += shaped.advance_x if not commands: raise ValueError( f"{_element_label(element)} has no glyph outline to materialize" ) return normalize_path_commands(commands) @lru_cache(maxsize=1) def _load_harfbuzz() -> Any: try: import uharfbuzz except ImportError as exc: raise RuntimeError( "Text operands in Shape Boolean require uharfbuzz. Install the " "project requirements, or run: pip install uharfbuzz" ) from exc required = ("Blob", "Buffer", "DrawFuncs", "Face", "Font", "StyleTag", "shape") missing = [name for name in required if not hasattr(uharfbuzz, name)] if missing: raise RuntimeError( "Installed uharfbuzz is incompatible; missing: " + ", ".join(missing) ) return uharfbuzz def _element_chain( element: ET.Element, parents: Mapping[ET.Element, ET.Element], ) -> list[ET.Element]: chain = [element] current = element while current in parents: current = parents[current] chain.append(current) chain.reverse() return chain def _validate_text_structure( element: ET.Element, chain: Sequence[ET.Element], ) -> None: if _local_name(element.tag) != "text": raise AssertionError("Text outline requires one element") children = list(element) if children: child_tags = ", ".join( f"<{_local_name(child.tag)}>" for child in children ) raise ValueError( f"{_element_label(element)} must contain direct single-line text; " f"child content is unsupported ({child_tags})" ) unsupported_attrs = sorted( _local_name(name) for name in element.attrib if _local_name(name) in _UNSUPPORTED_TEXT_ATTRIBUTES ) if unsupported_attrs: raise ValueError( f"{_element_label(element)} uses unsupported text attribute(s): " + ", ".join(unsupported_attrs) ) for current in chain: inline = parse_inline_style(current.get("style")) unsupported = sorted( name for name in _UNSUPPORTED_TEXT_PROPERTIES if current.get(name) is not None or name in inline ) if unsupported: raise ValueError( f"{_element_label(element)} inherits unsupported text " f"property/properties from {_element_label(current)}: " + ", ".join(unsupported) ) decoration = _effective_property(chain, "text-decoration") or "none" try: parsed_decoration = parse_project_text_decoration(decoration) except ValueError as exc: raise ValueError( f"{_element_label(element)} text-decoration={decoration!r}: {exc}" ) from exc if parsed_decoration.canonical != "none": raise ValueError( f"{_element_label(element)} uses text decoration; materialize the " "decoration separately before Shape Boolean" ) stroke = _effective_property(chain, "stroke") if stroke is not None and stroke.strip().lower() not in { "", "inherit", "none", "transparent", }: raise ValueError( f"{_element_label(element)} uses a visible text stroke; Shape " "Boolean materializes the filled glyph silhouette only" ) def _normalized_direct_text( element: ET.Element, chain: Sequence[ET.Element], ) -> str: xml_space = "default" for current in chain: try: xml_space = resolve_project_xml_space(current, xml_space) except ValueError as exc: raise ValueError( f"{_element_label(current)} xml:space: {exc}" ) from exc segments = normalize_project_text_segments([ (xml_space, element.text or ""), ]) return "".join(text for _owner, text in segments) def _resolve_text_style(chain: Sequence[ET.Element]) -> _TextStyle: font_size = 16.0 root_font_size = 16.0 letter_spacing = 0.0 family_value = "sans-serif" weight_value = "normal" style_value = "normal" anchor_value = "start" for index, current in enumerate(chain): inline = parse_inline_style(current.get("style")) raw_size = inline.get("font-size", current.get("font-size")) if raw_size is not None and raw_size.strip().lower() != "inherit": relative_base = ( root_font_size if raw_size.strip().lower().endswith("rem") else font_size ) try: font_size = parse_svg_length( raw_size, font_size, font_size=relative_base, ) font_px_to_hpt(font_size) except ValueError as exc: raise ValueError( f"{_element_label(current)} font-size={raw_size!r}: {exc}" ) from exc if index == 0: root_font_size = font_size raw_spacing = inline.get( "letter-spacing", current.get("letter-spacing"), ) if raw_spacing is not None and raw_spacing.strip().lower() != "inherit": try: letter_spacing = float(parse_project_letter_spacing( raw_spacing, font_size=font_size, ).value) except ValueError as exc: raise ValueError( f"{_element_label(current)} " f"letter-spacing={raw_spacing!r}: {exc}" ) from exc family_value = _updated_property( current, inline, "font-family", family_value, ) weight_value = _updated_property( current, inline, "font-weight", weight_value, ) style_value = _updated_property( current, inline, "font-style", style_value, ) anchor_value = _updated_property( current, inline, "text-anchor", anchor_value, ) families = _parse_font_family_stack(family_value) try: parsed_weight = parse_project_font_weight(weight_value) parsed_style = parse_project_font_style(style_value) parsed_anchor = parse_project_text_anchor(anchor_value) except ValueError as exc: raise ValueError( f"{_element_label(chain[-1])} has invalid text typography: {exc}" ) from exc weight = { "normal": 400, "bold": 700, }.get(parsed_weight.canonical) if weight is None: weight = int(parsed_weight.canonical) return _TextStyle( families=families, font_size=font_size, font_weight=weight, italic=bool(parsed_style.value), anchor=str(parsed_anchor.value), letter_spacing=letter_spacing, ) def _updated_property( element: ET.Element, inline: Mapping[str, str], name: str, inherited: str, ) -> str: raw = inline.get(name, element.get(name)) if raw is None or raw.strip().lower() == "inherit": return inherited value = raw.strip() if not value: raise ValueError(f"{_element_label(element)} {name} must not be empty") return value def _effective_property( chain: Sequence[ET.Element], name: str, ) -> str | None: value: str | None = None for current in chain: inline = parse_inline_style(current.get("style")) raw = inline.get(name, current.get(name)) if raw is not None and raw.strip().lower() != "inherit": value = raw.strip() return value def _text_position(element: ET.Element, name: str) -> float: raw = element.get(name) if raw is None: return 0.0 try: return parse_project_geometry_length(raw, name) except ValueError as exc: raise ValueError( f"{_element_label(element)} {name}={raw!r}: {exc}" ) from exc def _parse_font_family_stack(raw: str) -> tuple[str, ...]: families = tuple( value.strip().strip("'\"") for value in raw.split(",") ) if not families or any(not family for family in families): raise ValueError("font-family must contain at least one family name") return families def _normalize_explicit_font_dirs( font_dirs: Sequence[str | Path], ) -> tuple[str, ...]: if isinstance(font_dirs, (str, bytes)): raise ValueError( "Shape Boolean font_dirs must be a sequence of directory paths" ) roots: list[str] = [] seen: set[str] = set() for raw in font_dirs: root = Path(raw).expanduser() if not root.is_dir(): raise ValueError(f"Shape Boolean font directory not found: {root}") resolved = str(root.resolve()) if resolved not in seen: roots.append(resolved) seen.add(resolved) return tuple(roots) @lru_cache(maxsize=8) def _font_catalog(explicit_roots: tuple[str, ...]) -> tuple[_FontFace, ...]: hb = _load_harfbuzz() records: list[_FontFace] = [] for path in _font_files(explicit_roots): try: data = path.read_bytes() first_face = hb.Face(data, 0) face_count = max(1, int(first_face.count)) except (OSError, RuntimeError, ValueError): continue for index in range(face_count): try: face = hb.Face(data, index) families = _face_names(face, _FAMILY_NAME_IDS) if not families: continue font = hb.Font(face) weight_value = font.get_style_value(hb.StyleTag.WEIGHT) italic_value = font.get_style_value(hb.StyleTag.ITALIC) slant_value = font.get_style_value(hb.StyleTag.SLANT_ANGLE) axes = { axis.tag: axis for axis in face.axis_infos } coverage = frozenset(int(value) for value in face.unicodes) except (OSError, RuntimeError, TypeError, ValueError): continue weight = int(round(weight_value)) if math.isfinite(weight_value) else 400 weight = min(1000, max(1, weight)) italic = ( math.isfinite(italic_value) and italic_value >= 0.5 ) or ( math.isfinite(slant_value) and abs(slant_value) > 0.01 ) records.append(_FontFace( path=path, index=index, families=families, weight=weight, italic=italic, weight_range=_axis_range(axes.get("wght")), italic_range=_axis_range(axes.get("ital")), coverage=coverage, order=len(records), )) if not records: raise RuntimeError( "Text operands in Shape Boolean found no readable OpenType fonts; " "install fonts or pass --font-dir" ) return tuple(records) def _font_files(explicit_roots: tuple[str, ...]) -> tuple[Path, ...]: roots = [Path(value) for value in explicit_roots] roots.extend(_system_font_roots()) files: list[Path] = [] seen: set[str] = set() for root in roots: if not root.is_dir(): continue try: candidates = sorted( ( path for path in root.rglob("*") if path.is_file() and path.suffix.lower() in _FONT_EXTENSIONS ), key=lambda path: str(path).casefold(), ) except OSError: continue for path in candidates: try: key = str(path.resolve()) except OSError: key = str(path) if key not in seen: files.append(path) seen.add(key) return tuple(files) def _system_font_roots() -> tuple[Path, ...]: roots: list[Path] = [] home = Path.home() if sys.platform.startswith("win"): windows = Path(os.environ.get("WINDIR", "C:/Windows")) roots.append(windows / "Fonts") local_app_data = os.environ.get("LOCALAPPDATA") if local_app_data: roots.append(Path(local_app_data) / "Microsoft/Windows/Fonts") elif sys.platform == "darwin": roots.extend(( Path("/System/Library/Fonts"), Path("/Library/Fonts"), home / "Library/Fonts", )) else: xdg_data_home = os.environ.get("XDG_DATA_HOME") if xdg_data_home: roots.append(Path(xdg_data_home) / "fonts") roots.extend((home / ".local/share/fonts", home / ".fonts")) xdg_data_dirs = os.environ.get( "XDG_DATA_DIRS", "/usr/local/share:/usr/share", ) roots.extend( Path(value) / "fonts" for value in xdg_data_dirs.split(os.pathsep) if value ) return tuple(roots) def _face_names(face: Any, name_ids: Sequence[int]) -> tuple[str, ...]: values: list[str] = [] seen: set[str] = set() for name_id in name_ids: value = face.get_name(name_id) if not value: continue normalized = " ".join(str(value).split()) key = _family_key(normalized) if normalized and key not in seen: values.append(normalized) seen.add(key) return tuple(values) def _font_candidates( catalog: Sequence[_FontFace], families: Sequence[str], target_weight: int, italic: bool, ) -> list[_FontFace]: by_family: dict[str, list[_FontFace]] = {} for face in catalog: for family in face.families: by_family.setdefault(_family_key(family), []).append(face) candidates: list[_FontFace] = [] seen: set[tuple[Path, int]] = set() for requested in families: requested_key = _family_key(requested) generic = requested_key in _GENERIC_FAMILIES expanded = _GENERIC_FAMILIES.get(requested_key, (requested,)) for family in expanded: available = by_family.get(_family_key(family), ()) if generic: available = tuple( face for face in available if _face_supports_style( face, target_weight, italic, ) ) matches = sorted( available, key=lambda face: ( not _face_supports_style( face, target_weight, italic, ), face.italic != italic, abs(face.weight - target_weight), face.order, ), ) for face in matches: key = (face.path, face.index) if key not in seen: candidates.append(face) seen.add(key) return candidates def _face_supports_style( face: _FontFace, requested_weight: int, requested_italic: bool, ) -> bool: weight_supported = ( face.weight == requested_weight or ( face.weight_range is not None and face.weight_range[0] <= requested_weight <= face.weight_range[1] ) ) italic_value = 1.0 if requested_italic else 0.0 italic_supported = ( face.italic == requested_italic or ( face.italic_range is not None and face.italic_range[0] <= italic_value <= face.italic_range[1] ) ) return weight_supported and italic_supported def _axis_range(axis: Any | None) -> tuple[float, float] | None: if axis is None: return None return (float(axis.min_value), float(axis.max_value)) def _font_runs( element: ET.Element, clusters: Sequence[str], candidates: Sequence[_FontFace], families: Sequence[str], ) -> list[_FontRun]: selected: list[tuple[str, _FontFace]] = [] previous_face: _FontFace | None = None for cluster in clusters: required = _required_codepoints(cluster) face = next( ( candidate for candidate in candidates if required.issubset(candidate.coverage) ), None, ) if face is None and not required and previous_face is not None: face = previous_face if face is None: codepoints = " ".join( f"U+{ord(character):04X}" for character in cluster ) raise ValueError( f"{_element_label(element)} cannot resolve glyphs for " f"{codepoints} from font stack {', '.join(families)!r}" ) selected.append((cluster, face)) previous_face = face runs: list[_FontRun] = [] run_clusters: list[str] = [] run_face: _FontFace | None = None run_start = 0 for index, (cluster, face) in enumerate(selected): if run_face is None or face is run_face: run_clusters.append(cluster) run_face = face continue runs.append(_FontRun( face=run_face, clusters=tuple(run_clusters), global_cluster_start=run_start, )) run_clusters = [cluster] run_face = face run_start = index if run_face is not None: runs.append(_FontRun( face=run_face, clusters=tuple(run_clusters), global_cluster_start=run_start, )) return runs def _shape_runs( runs: Sequence[_FontRun], font_size: float, font_weight: int, italic: bool, letter_spacing: float, ) -> list[_ShapedRun]: hb = _load_harfbuzz() font_cache: dict[tuple[Path, int], tuple[Any, int]] = {} shaped: list[_ShapedRun] = [] for run in runs: key = (run.face.path, run.face.index) cached = font_cache.get(key) if cached is None: data = run.face.path.read_bytes() face = hb.Face(data, run.face.index) upem = int(face.upem) if upem <= 0: raise ValueError( f"Font {run.face.path} face {run.face.index} has invalid UPEM" ) font = hb.Font(face) font.scale = (upem, upem) _apply_font_style( run.face, face, font, font_weight, italic, ) cached = (font, upem) font_cache[key] = cached font, upem = cached buffer = hb.Buffer() buffer.add_str(run.text) buffer.guess_segment_properties() if str(buffer.direction) != "ltr": raise ValueError( f"Text run {run.text!r} resolves to {buffer.direction} " "direction; only horizontal left-to-right text is supported" ) if letter_spacing: hb.shape(font, buffer, {"liga": False, "clig": False}) else: hb.shape(font, buffer) infos = tuple(buffer.glyph_infos) positions = tuple(buffer.glyph_positions) if not infos or len(infos) != len(positions): raise ValueError(f"Font shaping produced no glyphs for {run.text!r}") scale = font_size / upem advance_x = sum(position.x_advance for position in positions) * scale shaped.append(_ShapedRun( run=run, font=font, infos=infos, positions=positions, scale=scale, advance_x=advance_x, )) return shaped def _apply_font_style( record: _FontFace, face: Any, font: Any, requested_weight: int, requested_italic: bool, ) -> None: axes = { axis.tag: axis for axis in face.axis_infos } variations: dict[str, float] = {} weight_axis = axes.get("wght") if weight_axis is not None: if not weight_axis.min_value <= requested_weight <= weight_axis.max_value: raise ValueError( f"Font {record.path.name} cannot provide requested weight " f"{requested_weight}; its wght axis is " f"{weight_axis.min_value:g}..{weight_axis.max_value:g}" ) variations["wght"] = float(requested_weight) elif record.weight != requested_weight: raise ValueError( f"Font {record.path.name} resolves weight {record.weight}, not " f"requested {requested_weight}; install the exact face, choose a " "matching family, or adjust the text operand" ) italic_axis = axes.get("ital") italic_value = 1.0 if requested_italic else 0.0 if italic_axis is not None: if not italic_axis.min_value <= italic_value <= italic_axis.max_value: style = "italic" if requested_italic else "normal" raise ValueError( f"Font {record.path.name} cannot provide requested {style} " "style through its ital axis" ) variations["ital"] = italic_value elif record.italic != requested_italic: requested = "italic" if requested_italic else "normal" resolved = "italic" if record.italic else "normal" raise ValueError( f"Font {record.path.name} resolves {resolved}, not requested " f"{requested}; install the exact face, choose a matching family, " "or adjust the text operand" ) if variations: font.set_variations(variations) def _draw_functions(hb: Any) -> Any: functions = hb.DrawFuncs() def move_to(x: float, y: float, state: _DrawState) -> None: point = state.point(x, y) state.commands.append(PathCommand("M", [*point])) def line_to(x: float, y: float, state: _DrawState) -> None: point = state.point(x, y) state.commands.append(PathCommand("L", [*point])) state.segment_count += 1 def quadratic_to( control_x: float, control_y: float, x: float, y: float, state: _DrawState, ) -> None: control = state.point(control_x, control_y) point = state.point(x, y) state.commands.append(PathCommand( "Q", [control[0], control[1], point[0], point[1]], )) state.segment_count += 1 def cubic_to( control_1_x: float, control_1_y: float, control_2_x: float, control_2_y: float, x: float, y: float, state: _DrawState, ) -> None: control_1 = state.point(control_1_x, control_1_y) control_2 = state.point(control_2_x, control_2_y) point = state.point(x, y) state.commands.append(PathCommand( "C", [ control_1[0], control_1[1], control_2[0], control_2[1], point[0], point[1], ], )) state.segment_count += 1 def close_path(state: _DrawState) -> None: state.commands.append(PathCommand("Z", [])) functions.set_move_to_func(move_to) functions.set_line_to_func(line_to) functions.set_quadratic_to_func(quadratic_to) functions.set_cubic_to_func(cubic_to) functions.set_close_path_func(close_path) return functions def _cluster_offsets(clusters: Sequence[str]) -> list[int]: offsets: list[int] = [] offset = 0 for cluster in clusters: offsets.append(offset) offset += len(cluster) return offsets def _required_codepoints(cluster: str) -> frozenset[int]: return frozenset( ord(character) for character in cluster if not _is_default_ignorable(character) ) def _is_default_ignorable(character: str) -> bool: codepoint = ord(character) return ( unicodedata.category(character) == "Cf" or 0x180B <= codepoint <= 0x180D or 0x2061 <= codepoint <= 0x206F or 0xFE00 <= codepoint <= 0xFE0F or 0xE0100 <= codepoint <= 0xE01EF ) def _cluster_may_have_no_outline(cluster: str) -> bool: return all( character.isspace() or _is_default_ignorable(character) for character in cluster ) def _family_key(value: str) -> str: return " ".join(value.split()).casefold() def _local_name(tag: object) -> str: raw = str(tag) return raw.rsplit("}", 1)[-1] if "}" in raw else raw def _element_label(element: ET.Element) -> str: tag = _local_name(element.tag) element_id = element.get("id") return f"<{tag} id={element_id!r}>" if element_id else f"<{tag}>"