Font Replacement - suggested changes ()

I am attaching a script with improvements to the “Font Replacement” script added to pyRevit 5.2.
Improvements:

  • in the GUI, the 1st line of text is split into several (not all text was displayed).
  • added support for nested families of the Generic Annotation category.
  • added work in a family (replacement of fonts in the current family and in nested Generic Annotations).
  • added renaming of text style sizes and sizes if they contained the name of the original font.

Thank you very much for the script! It appeared very timely (I was going to write it myself).
P.S. I still do not know how to work well with GitHub and Pull Request. I apologize. Therefore, I am posting my code here. I hope you will find something useful from it.

Summary
# encoding: utf-8
import System
import re

from rpw.ui.forms import (
    FlexForm,
    Label,
    ComboBox,
    Separator,
    Button,
    CheckBox,
)

from pyrevit import script, DB, HOST_APP, revit
from pyrevit.forms import alert, ProgressBar
from pyrevit.revit.db.create import FamilyLoaderOptionsHandler

output = script.get_output()
logger = script.get_logger()


def rename_element_type_if_needed(element_type, old_font, new_font):
    # type: (DB.ElementType, str, str) -> None
    """Renames an element type if its name contains the old font name (case-insensitive).
    Args:
        element_type (DB.ElementType): The element type to rename.
        old_font (str): The old font name.
        new_font (str): The new font name.
    Returns:
        None
    """
    current_name = DB.Element.Name.GetValue(element_type)
    # Use case-insensitive check to see if the old font name is part of the type name
    if old_font.lower() in current_name.lower():
        # Use case-insensitive replace
        try:
            new_name = re.sub(re.escape(old_font), new_font, current_name, flags=re.IGNORECASE)
            if new_name != current_name:
                DB.Element.Name.SetValue(element_type, new_name)
                logger.debug("Renamed type from '{}' to '{}'".format(current_name, new_name))
        except Exception as e:
            # Log error if renaming fails, but don't stop the script
            logger.error("Failed to rename type '{}' to '{}': {}".format(current_name, new_name, e))


def update_nested_generic_annotations(host_family_doc, font_name):
    # type: (DB.Document, str) -> tuple[list[tuple[str, str, str]], bool]
    """
    Recursively finds and updates fonts in nested generic annotation families.
    Args:
        host_family_doc (DB.Document): The host family document.
        font_name (str): The new font name.
    Returns:
        tuple: A tuple containing:
            - result (list): A list of tuples with details of updated elements.
            - found (bool): A boolean indicating if any updates were made.
    """
    results = []
    found_in_any_nested = False

    # Find nested families of category "Generic Annotation"
    nested_families = [
        f
        for f in DB.FilteredElementCollector(host_family_doc)
        .OfClass(DB.Family)
        .ToElements()
        if f.FamilyCategory
        and f.FamilyCategory.Id.IntegerValue
        == int(DB.BuiltInCategory.OST_GenericAnnotation)
    ]

    for nested_family in nested_families:
        if not nested_family.IsEditable:
            continue

        try:
            nested_family_doc = host_family_doc.EditFamily(nested_family)

            found_in_this_nested = False

            # Update text types within this nested family
            bip = DB.BuiltInParameter.TEXT_FONT
            element_types = (
                DB.FilteredElementCollector(nested_family_doc)
                .OfClass(DB.ElementType)
                .ToElements()
            )

            with revit.Transaction("Update Font in Nested Family", nested_family_doc):
                for et in element_types:
                    param = et.get_Parameter(bip)
                    if param and param.HasValue:
                        old_font = param.AsString()
                        if old_font != font_name:
                            rename_element_type_if_needed(et, old_font, font_name)
                            param.Set(font_name)
                            found_in_this_nested = True
                            # Report the name of the type that was changed
                            results.append(
                                (DB.Element.Name.GetValue(et), old_font, font_name)
                            )

                # Recursive call for families nested even deeper
                nested_results, nested_found = update_nested_generic_annotations(
                    nested_family_doc, font_name
                )
                if nested_found:
                    results.extend(nested_results)
                    found_in_this_nested = True

            if found_in_this_nested:
                found_in_any_nested = True
                # Load the updated nested family back into its host family
                nested_family_doc.LoadFamily(host_family_doc, FamilyLoaderOptionsHandler())

            nested_family_doc.Close(False)
        except Exception as e:
            logger.error(
                "Error processing nested family '{}' in '{}': {}".format(
                    nested_family.Name, host_family_doc.Title, e
                )
            )
            continue

    return results, found_in_any_nested


def process_family_document(family_doc, new_font_name):
    # type: (DB.Document, str) -> tuple[list[tuple[str, str, str]], bool]
    """
    Updates all applicable fonts in the given family document, including nested ones.
    Args:
        family_doc (DB.Document): The family document to process.
        new_font_name (str): The new font name.
    Returns:
        tuple: A tuple containing:
            - result (list): A list of tuples with details of updated elements.
            - found (bool): A boolean indicating if any updates were made.
    """
    result = []
    found = False
    bip = DB.BuiltInParameter.TEXT_FONT

    with revit.TransactionGroup("Update Font in Family and Nested", family_doc):
        # Update types in the main family document
        with revit.Transaction("Update Font in Family", family_doc):
            element_types = (
                DB.FilteredElementCollector(family_doc)
                .OfClass(DB.ElementType)
                .ToElements()
            )
            
            for element_type in element_types:
                try:
                    param = element_type.get_Parameter(bip)
                    if param and param.HasValue:
                        old_font = param.AsString()
                        if old_font != new_font_name:
                            rename_element_type_if_needed(element_type, old_font, new_font_name)
                            param.Set(new_font_name)
                            found = True
                            result.append(
                                (
                                    DB.Element.Name.GetValue(element_type),
                                    old_font,
                                    new_font_name,
                                )
                            )
                except Exception as e:
                    logger.error(
                        "Error updating type '{}' in family '{}': {}".format(
                            DB.Element.Name.GetValue(element_type),
                            family_doc.Title,
                            str(e),
                        )
                    )
                    continue

        # Process nested generic annotations
        nested_results, nested_found = update_nested_generic_annotations(
            family_doc, new_font_name)
        if nested_found:
            found = True
            result.extend(nested_results)
    
    return result, found


def update_text_font_in_family(doc, family, font_name):
    # type: (DB.Document, DB.Family, str) -> tuple[list[tuple[str, str, str]], bool]
    """
    Updates the text font in a Revit family to the specified font name.
    Also processes nested generic annotation families.
    Args:
        doc (DB.Document): The current Revit document.
        family (DB.Family): The Revit family to update.
        font_name (str): The new font name to set for text elements in the family.
    Returns:
        tuple: A tuple containing:
            - result (list): A list of tuples with details of updated elements in the format
              (element type name, old font name, new font name).
            - found (bool): A boolean indicating whether any text font parameters were found and updated.
    Raises:
        SystemError: If a system-level error occurs during the operation.
        ValueError: If an invalid value is encountered during the operation.
    """
    try:
        family_doc = doc.EditFamily(family)
        result, found = process_family_document(family_doc, font_name)

        if found:
            family_doc.LoadFamily(doc, FamilyLoaderOptionsHandler())
        family_doc.Close(False)
        return result, found
    except (SystemError, ValueError) as e:
        logger.error("Could not process family '{}': {}".format(family.Name, e))
        return [], False


def update_text_types(doc, element_types, font_name):
    # type: (DB.Document, list[DB.ElementType], str) -> list[tuple]
    """
    Updates the font of specified text element types in a Revit document.
    Args:
        doc (DB.Document): The Revit document where the text types are located.
        element_types (list[DB.ElementType]): A list of text element types to update.
        font_name (str): The new font name to set for the text types.
    Returns:
        list[tuple]: A list of tuples containing the element type name, the old font name,
                     and the new font name for each successfully updated text type.
    Raises:
        Exception: Logs an error message if updating a specific element type fails.
    """
    results = []
    bip = DB.BuiltInParameter.TEXT_FONT
    with revit.Transaction("Update Font in Types", doc):
        for elem_type in element_types:
            try:
                # Get the font parameter
                param = elem_type.get_Parameter(bip)
                if param and param.HasValue:
                    old_font = param.AsString()
                    if old_font != font_name:
                        rename_element_type_if_needed(elem_type, old_font, font_name)
                        param.Set(font_name)
                        results.append(
                            (DB.Element.Name.GetValue(elem_type), old_font, font_name)
                        )
            except Exception as e:
                logger.error(
                    "**Error updating {}: {}**".format(
                        DB.Element.Name.GetValue(elem_type), str(e)
                    )
                )
    return results


def main():
    doc = HOST_APP.doc
    uidoc = HOST_APP.uidoc

    # Get list of system fonts
    font_families = System.Drawing.FontFamily.Families
    font_names = [font_family.Name for font_family in font_families]
    font_names.sort()

    # --- LOGIC FOR FAMILY DOCUMENT ---
    if doc.IsFamilyDocument:
        components = [
            Label("Replace all fonts in this family."),
            Separator(),
            Label("Select Target Font:"),
            ComboBox("font", options=font_names, default="Arial"),
            Button("OK"),
        ]
        form = FlexForm("Replace Fonts in Family", components)
        form.show()
        if not form.values:
            return

        selected_font = form.values["font"]
        output.print_md(
            "# Updating fonts in current family to: **{}**".format(selected_font)
        )

        # The current document is a family document
        result, found = process_family_document(doc, selected_font)

        if found:
            output.print_md("## Updated Types:")
            for r in result:
                output.print_md(
                    "- Updated **{}**: from *{}* to **{}**".format(r[0], r[1], r[2])
                )
            output.print_md("\n**Total types updated: {}**".format(len(result)))
            alert("Fonts updated successfully.", title="Success")
        else:
            alert(
                "No text or dimension types with fonts found to update.",
                title="Information",
            )
        return  # End script execution for family document

    # --- LOGIC FOR PROJECT DOCUMENT ---
    components = [
        Label("Change all text fonts in the project to a new font."),
        Label("This will update all text fonts in families, dimensions,"),
        Label("and text notes. This will NOT update fonts in"),
        Label("schedules or tags."),
        Separator(),
        Label("Pick Target Font:"),
        ComboBox("font", options=font_names, default="Arial"),
        Separator(),
        Label("Elements to change:"),
        CheckBox("families", "Fonts used in Families", default=True),
        CheckBox("textnotes", "Text Notes Fonts in Project", default=True),
        CheckBox("dimensions", "Dimensions Fonts in Project", default=True),
        Button("OK"),
    ]

    form = FlexForm("Change Font in Elements", components)
    form.show()
    if not form.values:
        alert("No values selected.")
        return

    selected_font = form.values["font"]
    output.print_md("# Update Fonts To: **{}**".format(selected_font))

    if form.values["families"]:
        families = [
            family
            for family in DB.FilteredElementCollector(doc)
            .OfClass(DB.Family)
            .ToElements()
            if family.IsEditable
        ]
        output.print_md("## Families Updated")
        families_updated = 0
        elements_updated = 0
        with ProgressBar(cancellable=True) as pb:
            i = 0
            with revit.TransactionGroup("Update Font in Families", doc):
                output.print_md(
                    "Updating {} families in the project...".format(len(families))
                )
                for family in families:
                    if family.IsInPlace:
                        continue
                    print("Updating family: {}".format(family.Name))
                    result, found = update_text_font_in_family(
                        doc, family, selected_font
                    )
                    if found:
                        families_updated += 1
                        for r in result:
                            elements_updated += 1
                            logger.debug("Updated {}: {} → {}".format(r[0], r[1], r[2]))
                    if pb.cancelled:
                        break
                    pb.update_progress(i, len(families))
                    i += 1
            output.print_md("\n**Total families updated: {}**".format(families_updated))
            output.print_md(
                "\n**Total elements updated in families: {}**".format(elements_updated)
            )

    text_types = []
    dim_types = []
    if form.values["textnotes"]:
        text_types = (
            DB.FilteredElementCollector(doc).OfClass(DB.TextNoteType).ToElements()
        )
        output.print_md("## Text Note Types Updated")
        result = update_text_types(doc, text_types, selected_font)
        for r in result:
            logger.debug("Updated text note type: {}: {} → {}".format(r[0], r[1], r[2]))
        output.print_md("\n**Total text types updated: {}**".format(len(result)))

    if form.values["dimensions"]:
        dim_types = (
            DB.FilteredElementCollector(doc).OfClass(DB.DimensionType).ToElements()
        )
        output.print_md("## Dimension Types Updated")
        result = update_text_types(doc, dim_types, selected_font)
        for r in result:
            logger.debug("Updated dimension type: {}: {} → {}".format(r[0], r[1], r[2]))
        output.print_md("\n**Total dimension types updated: {}**".format(len(result)))

    uidoc.RefreshActiveView()


if __name__ == "__main__":
    main()
2 Likes

@Denver-22
thanks, nice addition,
I did make a few modifications here and there

Just for the sake of learning, regarding making changes in the code base:

  1. fork the repo (can be done directly on the website)
  2. change the code base in your fork
  3. commit the changes
  4. make a pull request against the upstream repo (pyrevitlabs/pyRevit) develop branch
  5. wait for reviews
  6. maintainers can merge the PR once the potential changes are made and reviewed

Latest WIP is building as I am typing.

Next step, a parameter replacer tool ;p

1 Like