Skip to content

FormattedTextArea

src.text_layers.FormattedTextArea

Bases: FormattedTextField

A FormattedTextField where the text is required to fit within a given area.

  • Reduces font size until the text fits within the reference layer's bounds.
  • Properly separates and formats flavor text with respect to an optional divider layer.
  • Centers the formatted text vertically with respect to the reference layer's bounds.
Source code in src\text_layers.py
class FormattedTextArea (FormattedTextField):
    """A FormattedTextField where the text is required to fit within a given area.

    * Reduces font size until the text fits within the reference layer's bounds.
    * Properly separates and formats flavor text with respect to an optional divider layer.
    * Centers the formatted text vertically with respect to the reference layer's bounds.
    """

    """
    * Properties
    """

    @cached_property
    def pt_reference(self) -> Optional[ReferenceLayer]:
        return self.kwargs.get('pt_reference', None)

    @cached_property
    def divider(self) -> Optional[Union[ArtLayer, LayerSet]]:
        """Divider layer, if provided and flavor text exists."""
        if (divider := self.kwargs.get('divider')) and all([self.flavor_text, self.contents, CFG.flavor_divider]):
            divider.visible = True
            return divider
        return

    @cached_property
    def scale_height(self) -> bool:
        """Scale text to fit reference height (Default: True)."""
        if scale_height := self.kwargs.get('scale_height'):
            return scale_height
        return True

    @cached_property
    def scale_width(self) -> bool:
        """Scale text to fit reference width (Default: False)."""
        if scale_width := self.kwargs.get('scale_width'):
            return scale_width
        return False

    @cached_property
    def fix_overflow_width(self) -> bool:
        """Scale text to fit bounding box width (Default: False)."""
        if fix_overflow_width := self.kwargs.get('fix_overflow_width'):
            return fix_overflow_width
        return True

    @cached_property
    def fix_overflow_height(self) -> bool:
        """Scale text to fit bounding box height (Default: If it overflows the bounds)."""
        if len(self.contents + self.flavor_text) > 280:
            return True
        if fix_overflow_height := self.kwargs.get('fix_overflow_height'):
            return fix_overflow_height
        return False

    """
    * Methods
    """

    def insert_divider(self):
        """Inserts and correctly positions flavor text divider."""

        # Create a reference layer with no effects
        flavor = self.layer.duplicate()
        rules = flavor.duplicate()
        flavor.rasterize(RasterizeType.EntireLayer)
        remove_trailing_text(rules, self.flavor_start)
        select_layer_bounds(rules, self.doc_selection)
        self.docref.activeLayer = flavor
        self.doc_selection.expand(2)
        self.doc_selection.clear()

        # Move flavor text to bottom, then position divider
        flavor.translate(0, self.layer.bounds[3] - flavor.bounds[3])
        position_between_layers(self.divider, rules, flavor)
        self.doc_selection.deselect()
        flavor.remove()
        rules.remove()

    def pre_scale_to_fit(self) -> Optional[float]:
        """Fix height overflow before formatting text."""
        contents = self.contents if not self.flavor_text else str(
            self.contents + "\r" + self.flavor_text)
        self.TI.contents = contents
        return scale_text_to_height(
            layer=self.layer,
            height=int(self.reference_dims['height']*1.1))

    def scale_to_fit(self, font_size: Optional[float] = None) -> None:
        """Scale font size to fit within any references."""

        # Scale layer to reference
        if self.reference_dims:

            # Resize the text until it fits the reference vertically
            if self.scale_height:
                font_size = scale_text_to_height(
                    layer=self.layer,
                    height=self.reference_dims['height'])

            # Resize the text until it fits the reference horizontally
            if self.scale_width:
                font_size = scale_text_to_width(
                    layer=self.layer,
                    width=self.reference_dims['width'],
                    step=0.2, font_size=font_size)

        # Resize the text until it fits the TextLayer bounding box
        if self.fix_overflow_width:
            scale_text_to_width_textbox(
                layer=self.layer, font_size=font_size)

    def position_within_reference(self):
        """Positions the layer with respect to the reference, if required."""

        # Ensure the layer is centered vertically
        dims = get_layer_dimensions(self.layer)
        self.layer.translate(0, self.reference_dims['center_y'] - dims['center_y'])

        # Ensure the layer is centered horizontally if needed
        if self.contents_centered and self.flavor_centered:
            self.layer.translate(0, self.reference_dims['center_x'] - dims['center_x'])

    def execute(self):

        # Skip if both are empty
        if not self.input:
            return
        font_size = None

        # Pre-scaling to prevent text overflow errors
        if self.fix_overflow_height and self.reference_dims:
            font_size = self.pre_scale_to_fit()

        # Execute text formatting
        super().execute()

        # Scale layer to fit reference
        self.scale_to_fit(font_size)

        # Position the layer properly
        if self.reference_dims:
            self.position_within_reference()

        # Insert flavor divider if needed
        if self.divider:
            self.insert_divider()

        # Shift vertically if the text overlaps the PT box
        if self.pt_reference:

            # Use newer methodology if top reference not provided
            delta = clear_reference_vertical(
                layer=self.layer,
                ref=self.pt_reference,
                docsel=self.doc_selection)

            # Shift the divider if layer was moved
            if delta < 0 and self.divider:
                self.divider.translate(0, delta)

Attributes

TI: TextItem

The TextItem object within the ArtLayer.

color: SolidColor

A SolidColor object provided, or fallback on current TextItem color.

divider: Optional[Union[ArtLayer, LayerSet]]

Divider layer, if provided and flavor text exists.

doc_selection: Selection

The Selection object from the active document.

docref: Document

The currently active Photoshop document.

fix_overflow_height: bool

Scale text to fit bounding box height (Default: If it overflows the bounds).

fix_overflow_width: bool

Scale text to fit bounding box width (Default: False).

flavor_color: Optional[SolidColor]

If defined separately, color is effectively the rules_color.

flavor_text_lead: Union[int, float]

Leading space before linebreak separating rules and flavor text. Increased if divider is present.

font: str

Font provided, or fallback on global constant.

font_bold: str

Bold font provided, or fallback on global constant.

font_italic: str

Italic font provided, or fallback on global constant.

font_mana: str

Mana font provided, or fallback on global constant.

is_text_layer: bool

Checks if the layer provided is a TextLayer.

kw_color: Optional[SolidColor]

Color to apply to the TextItem.

kw_font: Optional[str]

Font to apply to the root TextItem.

kw_font_bold: Optional[str]

Font to apply to any bold text in the TextItem.

kw_font_italic: Optional[str]

Font to apply to any italicized text in the TextItem.

kw_font_mana: Optional[str]

Font to apply to any mana symbols in the TextItem.

kw_symbol_map: dict[str, tuple[str, list[ColorObject]]]

Symbol map to use for formatting mana symbols.

kwargs: dict

Contains optional parameters to modify text formatting behavior.

layer: ArtLayer

ArtLayer containing the TextItem.

line_break_lead: Union[int, float]

Leading space before linebreaks.

reference: Optional[ArtLayer]

A reference layer, typically used for scaling the TextItem.

reference_dims: Optional[type[LayerDimensions]]

Optional[type[LayerDimensions]]: Dimensions of the scaling reference layer.

scale_height: bool

Scale text to fit reference height (Default: True).

scale_width: bool

Scale text to fit reference width (Default: False).

Functions

format_text()

Inserts rules and flavor text and formats it based on the defined mana symbols, italics text indices, and other predefined properties.

Source code in src\text_layers.py
def format_text(self):
    """Inserts rules and flavor text and formats it based on the defined mana
    symbols, italics text indices, and other predefined properties."""

    # Descriptors
    para_style = ActionDescriptor()
    para_range = ActionDescriptor()
    main_style = ActionDescriptor()
    main_range = ActionDescriptor()
    main_target = ActionDescriptor()
    main_desc = ActionDescriptor()

    # References
    main_ref = ActionReference()

    # Lists
    style_list = ActionList()
    main_list = ActionList()

    # Descriptor ID's
    idTo = sID('to')
    size = sID('size')
    idFrom = sID('from')
    textLayer = sID('textLayer')
    textStyle = sID('textStyle')
    ptUnit = sID('pointsUnit')
    spaceAfter = sID('spaceAfter')
    autoLeading = sID('autoLeading')
    startIndent = sID('startIndent')
    spaceBefore = sID('spaceBefore')
    leadingType = sID('leadingType')
    styleRange = sID('textStyleRange')
    paragraphStyle = sID('paragraphStyle')
    firstLineIndent = sID('firstLineIndent')
    fontPostScriptName = sID('fontPostScriptName')
    paragraphStyleRange = sID('paragraphStyleRange')

    # Spin up the text insertion action
    main_desc.putString(sID('textKey'), self.input)
    main_range.putInteger(idFrom, 0)
    main_range.putInteger(idTo, len(self.input))
    apply_color(main_style, self.color)
    main_style.putBoolean(autoLeading, False)
    main_style.putUnitDouble(size, ptUnit, self.font_size)
    main_style.putUnitDouble(sID('leading'), ptUnit, self.font_size)
    main_style.putString(fontPostScriptName, self.font)
    main_range.putObject(textStyle, textStyle, main_style)
    main_list.putObject(styleRange, main_range)

    # Bold the contents if necessary
    if self.rules_text and self.bold_rules_text:
        main_range.putInteger(idFrom, self.rules_start)
        main_range.putInteger(idTo, self.rules_end)
        main_style.putString(fontPostScriptName, self.font_bold)
        main_range.putObject(textStyle, textStyle, main_style)
        main_list.putObject(styleRange, main_range)

    # Italicize text from our italics indices
    if self.italics_indices:
        for start, end in self.italics_indices:
            main_range.putInteger(idFrom, start)
            main_range.putInteger(idTo, end)
            main_style.putString(fontPostScriptName, self.font_italic)
            main_range.putObject(textStyle, textStyle, main_style)
            main_list.putObject(styleRange, main_range)

    # Format each mana symbol
    if self.symbol_indices:
        for index, colors in self.symbol_indices:
            for i, color in enumerate(colors):
                main_range.putInteger(idFrom, index + i)
                main_range.putInteger(idTo, index + i + 1)
                apply_color(main_style, color)
                main_style.putString(fontPostScriptName, self.font_mana)
                main_range.putObject(textStyle, textStyle, main_style)
                main_list.putObject(styleRange, main_range)

    # Modal choice formatting
    if self.is_modal or self.is_flavor_text:
        para_range.putInteger(idFrom, 0)
        para_range.putInteger(idTo, len(self.input))
        para_style.putUnitDouble(firstLineIndent, ptUnit, 0)
        para_style.putUnitDouble(startIndent, ptUnit, 0)
        para_style.putUnitDouble(sID("endIndent"), ptUnit, 0)
        para_style.putUnitDouble(spaceBefore, ptUnit, self.line_break_lead)
        para_style.putUnitDouble(spaceAfter, ptUnit, 0)
        para_style.putInteger(sID("dropCapMultiplier"), 1)
        para_style.putEnumerated(leadingType, leadingType, sID("leadingBelow"))

    # Adjust paragraph formatting for modal card with bullet points
    if self.is_modal:
        default_style = ActionDescriptor()
        para_range.putInteger(idFrom, self.input.find("\u2022"))
        para_range.putInteger(idTo, self.input.rindex("\u2022") + 1)
        para_style.putUnitDouble(firstLineIndent, ptUnit, -CON.modal_indent)
        para_style.putUnitDouble(startIndent, ptUnit, CON.modal_indent)
        para_style.putUnitDouble(spaceBefore, ptUnit, 1)
        default_style.putString(fontPostScriptName, self.font_mana)
        default_style.putUnitDouble(size, ptUnit, 12)
        default_style.putBoolean(autoLeading, False)
        para_style.putObject(sID("defaultStyle"), textStyle, default_style)
        para_range.putObject(paragraphStyle, paragraphStyle, para_style)
        style_list.putObject(paragraphStyleRange, para_range)

    # Flavor text actions
    if self.is_flavor_text:

        # Add linebreak spacing between rules and flavor text
        para_range.putInteger(idFrom, self.flavor_start + 3)
        para_range.putInteger(idTo, self.flavor_start + 4)
        para_style.putUnitDouble(startIndent, ptUnit, 0)
        para_style.putUnitDouble(firstLineIndent, ptUnit, 0)
        para_style.putUnitDouble(sID("impliedStartIndent"), ptUnit, 0)
        para_style.putUnitDouble(sID("impliedFirstLineIndent"), ptUnit, 0)
        para_style.putUnitDouble(spaceBefore, ptUnit, self.flavor_text_lead)
        para_range.putObject(paragraphStyle, paragraphStyle, para_style)
        style_list.putObject(paragraphStyleRange, para_range)

        # Adjust flavor text color
        if self.flavor_color:
            main_range.PutInteger(idFrom, self.flavor_start)
            main_range.PutInteger(idTo, self.flavor_end)
            apply_color(main_style, self.flavor_color)
            main_style.putString(fontPostScriptName, self.font_italic)
            main_range.PutObject(textStyle, textStyle, main_style)
            main_list.putObject(styleRange, main_range)

        # Quote actions flavor text
        if self.is_quote_text:

            # Adjust line break spacing if there's a line break in the flavor text
            para_range.putInteger(idFrom, self.quote_index + 3)
            para_range.putInteger(idTo, len(self.input))
            para_style.putUnitDouble(spaceBefore, ptUnit, 0)
            para_range.putObject(paragraphStyle, paragraphStyle, para_style)
            style_list.putObject(paragraphStyleRange, para_range)

            # Optional, align quote credit to right
            if self.right_align_quote and '"\r—' in self.flavor_text:
                para_range.putInteger(idFrom, self.input.find('"\r—') + 2)
                para_range.putInteger(idTo, self.flavor_end)
                para_style.putBoolean(sID('styleSheetHasParent'), True)
                para_range.putEnumerated(sID('align'), sID('alignmentType'), sID('right'))
                para_range.putObject(paragraphStyle, paragraphStyle, para_style)
                style_list.putObject(paragraphStyleRange, para_range)

    # Apply action lists
    main_desc.putList(paragraphStyleRange, style_list)
    main_desc.putList(styleRange, main_list)

    # Push changes to text layer
    main_ref.putEnumerated(textLayer, sID("ordinal"), sID("targetEnum"))
    main_target.putReference(sID("target"), main_ref)
    main_target.putObject(idTo, textLayer, main_desc)
    APP.executeAction(sID("set"), main_target, NO_DIALOG)

insert_divider()

Inserts and correctly positions flavor text divider.

Source code in src\text_layers.py
def insert_divider(self):
    """Inserts and correctly positions flavor text divider."""

    # Create a reference layer with no effects
    flavor = self.layer.duplicate()
    rules = flavor.duplicate()
    flavor.rasterize(RasterizeType.EntireLayer)
    remove_trailing_text(rules, self.flavor_start)
    select_layer_bounds(rules, self.doc_selection)
    self.docref.activeLayer = flavor
    self.doc_selection.expand(2)
    self.doc_selection.clear()

    # Move flavor text to bottom, then position divider
    flavor.translate(0, self.layer.bounds[3] - flavor.bounds[3])
    position_between_layers(self.divider, rules, flavor)
    self.doc_selection.deselect()
    flavor.remove()
    rules.remove()

position_within_reference()

Positions the layer with respect to the reference, if required.

Source code in src\text_layers.py
def position_within_reference(self):
    """Positions the layer with respect to the reference, if required."""

    # Ensure the layer is centered vertically
    dims = get_layer_dimensions(self.layer)
    self.layer.translate(0, self.reference_dims['center_y'] - dims['center_y'])

    # Ensure the layer is centered horizontally if needed
    if self.contents_centered and self.flavor_centered:
        self.layer.translate(0, self.reference_dims['center_x'] - dims['center_x'])

pre_scale_to_fit() -> Optional[float]

Fix height overflow before formatting text.

Source code in src\text_layers.py
def pre_scale_to_fit(self) -> Optional[float]:
    """Fix height overflow before formatting text."""
    contents = self.contents if not self.flavor_text else str(
        self.contents + "\r" + self.flavor_text)
    self.TI.contents = contents
    return scale_text_to_height(
        layer=self.layer,
        height=int(self.reference_dims['height']*1.1))

scale_to_fit(font_size: Optional[float] = None) -> None

Scale font size to fit within any references.

Source code in src\text_layers.py
def scale_to_fit(self, font_size: Optional[float] = None) -> None:
    """Scale font size to fit within any references."""

    # Scale layer to reference
    if self.reference_dims:

        # Resize the text until it fits the reference vertically
        if self.scale_height:
            font_size = scale_text_to_height(
                layer=self.layer,
                height=self.reference_dims['height'])

        # Resize the text until it fits the reference horizontally
        if self.scale_width:
            font_size = scale_text_to_width(
                layer=self.layer,
                width=self.reference_dims['width'],
                step=0.2, font_size=font_size)

    # Resize the text until it fits the TextLayer bounding box
    if self.fix_overflow_width:
        scale_text_to_width_textbox(
            layer=self.layer, font_size=font_size)

validate()

Ensure the Text Layer provided is valid.

Source code in src\text_layers.py
def validate(self):
    """Ensure the Text Layer provided is valid."""
    if self.layer and self.is_text_layer:
        # Layer is valid, select and show it
        select_layer(self.layer, True)
        return True
    with suppress(Exception):
        # Layer provided doesn't exist or isn't a text layer
        name = self.layer.name if self.layer else '[Non-Layer]'
        print(f'Text Field class: {self.__class__.__name__}\n'
              f'Invalid layer provided: {name}')
        self.layer.visible = False
    return False