fortplot_mathtext.f90 Source File


Source Code

module fortplot_mathtext
    !! Mathematical text rendering with superscripts and subscripts
    !! Supports matplotlib-like syntax: x^2, y_i, x_{text}, y^{superscript}
    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_unicode, only: utf8_to_codepoint, utf8_char_length
    implicit none

    private
    public :: mathtext_element_t, parse_mathtext, render_mathtext_elements
    public :: calculate_mathtext_width, calculate_mathtext_height
    public :: ELEMENT_NORMAL, ELEMENT_SUPERSCRIPT, ELEMENT_SUBSCRIPT, ELEMENT_SQRT

    ! Mathematical text element types
    integer, parameter :: ELEMENT_NORMAL = 0
    integer, parameter :: ELEMENT_SUPERSCRIPT = 1
    integer, parameter :: ELEMENT_SUBSCRIPT = 2
    integer, parameter :: ELEMENT_SQRT = 3

    ! Font scaling factors (matching matplotlib's approach)
    real(wp), parameter :: SHRINK_FACTOR = 0.7_wp  ! Super/subscript size ratio
    real(wp), parameter :: SUPERSCRIPT_RAISE = 0.6_wp  ! Fraction of font height to raise
    real(wp), parameter :: SUBSCRIPT_LOWER = 0.2_wp   ! Fraction of font height to lower

    type :: mathtext_element_t
        character(len=:), allocatable :: text
        integer :: element_type = ELEMENT_NORMAL
        real(wp) :: font_size_ratio = 1.0_wp
        real(wp) :: vertical_offset = 0.0_wp  ! In pixels, positive = up
    end type mathtext_element_t

contains

    function parse_mathtext(input_text) result(elements)
        !! Parse mathematical text into renderable elements
        character(len=*), intent(in) :: input_text
        type(mathtext_element_t), allocatable :: elements(:)

        integer :: i, n, brace_count, current_len
        character(len=len(input_text)) :: current_text
        logical :: in_superscript, in_subscript, in_braces
        type(mathtext_element_t) :: temp_elements(len(input_text))
        integer :: element_count
        integer :: start_idx, byte

        element_count = 0
        n = len_trim(input_text)
        i = 1
        current_text = ''
        current_len = 0  ! Track actual length of current_text content
        in_superscript = .false.
        in_subscript = .false.
        in_braces = .false.
        brace_count = 0

        do while (i <= n)
            if (input_text(i:i) == '^') then
                ! Split last character from current text if it exists
                if (current_len > 0) then
                    start_idx = current_len
                    do while (start_idx > 1)
                        byte = ichar(current_text(start_idx:start_idx))
                        if (iand(byte, 192) /= 128) exit
                        start_idx = start_idx - 1
                    end do
                    if (start_idx > 1) then
                        element_count = element_count + 1
                        call create_element(temp_elements(element_count), &
                                          current_text(1:start_idx-1), &
                                          ELEMENT_NORMAL, 1.0_wp, 0.0_wp)
                    end if
                    element_count = element_count + 1
                    call create_element(temp_elements(element_count), &
                                      current_text(start_idx:current_len), &
                                      ELEMENT_NORMAL, 1.0_wp, 0.0_wp)
                end if
                current_text = ''
                current_len = 0

                i = i + 1
                call parse_superscript_subscript(input_text, i, n, temp_elements, &
                                               element_count, ELEMENT_SUPERSCRIPT)

            else if (input_text(i:i) == '_') then
                ! Split last character from current text if it exists
                if (current_len > 0) then
                    start_idx = current_len
                    do while (start_idx > 1)
                        byte = ichar(current_text(start_idx:start_idx))
                        if (iand(byte, 192) /= 128) exit
                        start_idx = start_idx - 1
                    end do
                    if (start_idx > 1) then
                        element_count = element_count + 1
                        call create_element(temp_elements(element_count), &
                                          current_text(1:start_idx-1), &
                                          ELEMENT_NORMAL, 1.0_wp, 0.0_wp)
                    end if
                    element_count = element_count + 1
                    call create_element(temp_elements(element_count), &
                                      current_text(start_idx:current_len), &
                                      ELEMENT_NORMAL, 1.0_wp, 0.0_wp)
                end if
                current_text = ''
                current_len = 0

                i = i + 1
                call parse_superscript_subscript(input_text, i, n, temp_elements, &
                                               element_count, ELEMENT_SUBSCRIPT)

            else if (input_text(i:i) == '\') then
                if (i+4 <= n) then
                    if (input_text(i+1:min(i+4,n)) == 'sqrt') then
                        if (current_len > 0) then
                            element_count = element_count + 1
                            call create_element(temp_elements(element_count), &
                                              current_text(1:current_len), &
                                              ELEMENT_NORMAL, 1.0_wp, 0.0_wp)
                        end if
                        current_text = ''
                        current_len = 0
                        i = i + 5
                        call parse_sqrt_content(input_text, i, n, temp_elements, element_count)
                        cycle
                    end if
                end if

                if (i + 1 <= n) then
                    select case (input_text(i+1:i+1))
                    case ('_', '^', '$', '\')
                        current_len = current_len + 1
                        current_text(current_len:current_len) = input_text(i+1:i+1)
                        i = i + 2
                    case default
                        current_len = current_len + 1
                        current_text(current_len:current_len) = input_text(i:i)
                        i = i + 1
                    end select
                else
                    current_len = current_len + 1
                    current_text(current_len:current_len) = input_text(i:i)
                    i = i + 1
                end if
            else
                current_len = current_len + 1
                current_text(current_len:current_len) = input_text(i:i)
                i = i + 1
            end if
        end do

        ! Store any remaining text (preserve spaces)
        if (current_len > 0) then
            element_count = element_count + 1
            call create_element(temp_elements(element_count), &
                              current_text(1:current_len), &
                              ELEMENT_NORMAL, 1.0_wp, 0.0_wp)
        end if

        ! Allocate and copy final elements
        allocate(elements(element_count))
        elements(1:element_count) = temp_elements(1:element_count)

    end function parse_mathtext

    subroutine parse_superscript_subscript(input_text, start_i, n, elements, &
                                           element_count, element_type)
        !! Parse superscript or subscript content
        character(len=*), intent(in) :: input_text
        integer, intent(inout) :: start_i
        integer, intent(in) :: n
        type(mathtext_element_t), intent(inout) :: elements(:)
        integer, intent(inout) :: element_count
        integer, intent(in) :: element_type

        character(len=n) :: script_text
        integer :: i, brace_count, script_len
        logical :: in_braces
        real(wp) :: font_size_ratio, vertical_offset

        script_text = ''
        script_len = 0  ! Track actual length of script_text content
        i = start_i

        if (i > n) return

        ! Check if we have braces for multi-character script
        if (input_text(i:i) == '{') then
            in_braces = .true.
            brace_count = 1
            i = i + 1  ! Skip opening brace

            do while (i <= n .and. brace_count > 0)
                if (input_text(i:i) == '{') then
                    brace_count = brace_count + 1
                    script_len = script_len + 1
                    script_text(script_len:script_len) = input_text(i:i)
                else if (input_text(i:i) == '}') then
                    brace_count = brace_count - 1
                    if (brace_count > 0) then
                        script_len = script_len + 1
                        script_text(script_len:script_len) = input_text(i:i)
                    end if
                else
                    script_len = script_len + 1
                    script_text(script_len:script_len) = input_text(i:i)
                end if
                i = i + 1
            end do
        else
            ! Single character script
            script_len = 1
            script_text(1:1) = input_text(i:i)
            i = i + 1
        end if

        ! Set font size and vertical offset
        font_size_ratio = SHRINK_FACTOR
        if (element_type == ELEMENT_SUPERSCRIPT) then
            vertical_offset = SUPERSCRIPT_RAISE  ! Positive to move up in PDF coordinates
        else if (element_type == ELEMENT_SUBSCRIPT) then
            vertical_offset = -SUBSCRIPT_LOWER  ! Negative to move down in PDF coordinates
        else
            vertical_offset = 0.0_wp
        end if

        ! Create element
        if (script_len > 0) then
            element_count = element_count + 1
            call create_element(elements(element_count), script_text(1:script_len), &
                              element_type, font_size_ratio, vertical_offset)
        end if

        start_i = i
    end subroutine parse_superscript_subscript

    subroutine parse_sqrt_content(input_text, start_i, n, elements, element_count)
        !! Parse square root content
        character(len=*), intent(in) :: input_text
        integer, intent(inout) :: start_i
        integer, intent(in) :: n
        type(mathtext_element_t), intent(inout) :: elements(:)
        integer, intent(inout) :: element_count

        character(len=n) :: rad_text
        integer :: i, brace_count, rad_len

        rad_text = ''
        rad_len = 0
        i = start_i

        if (i > n) return

        if (input_text(i:i) == '{') then
            brace_count = 1
            i = i + 1
            do while (i <= n .and. brace_count > 0)
                if (input_text(i:i) == '{') then
                    brace_count = brace_count + 1
                    rad_len = rad_len + 1
                    rad_text(rad_len:rad_len) = input_text(i:i)
                else if (input_text(i:i) == '}') then
                    brace_count = brace_count - 1
                    if (brace_count > 0) then
                        rad_len = rad_len + 1
                        rad_text(rad_len:rad_len) = input_text(i:i)
                    end if
                else
                    rad_len = rad_len + 1
                    rad_text(rad_len:rad_len) = input_text(i:i)
                end if
                i = i + 1
            end do
        else
            rad_len = 1
            rad_text(1:1) = input_text(i:i)
            i = i + 1
        end if

        if (rad_len > 0) then
            element_count = element_count + 1
            call create_element(elements(element_count), rad_text(1:rad_len), &
                              ELEMENT_SQRT, 1.0_wp, 0.0_wp)
        end if

        start_i = i
    end subroutine parse_sqrt_content

    subroutine create_element(element, text, element_type, font_size_ratio, vertical_offset)
        !! Create a mathtext element with proper string handling
        type(mathtext_element_t), intent(out) :: element
        character(len=*), intent(in) :: text
        integer, intent(in) :: element_type
        real(wp), intent(in) :: font_size_ratio, vertical_offset

        ! Store text properly - preserve all spaces
        element%text = text
        element%element_type = element_type
        element%font_size_ratio = font_size_ratio
        element%vertical_offset = vertical_offset
    end subroutine create_element

    function calculate_mathtext_width(elements, base_font_size) result(total_width)
        !! Calculate total width of mathematical text elements
        !! This function signature is used by text_rendering but implementation moved there
        type(mathtext_element_t), intent(in) :: elements(:)
        real(wp), intent(in) :: base_font_size
        integer :: total_width

        ! This is a placeholder - actual implementation is in text_rendering module
        ! to avoid circular dependencies
        total_width = 0

        ! Suppress unused parameter warnings
        associate(unused_elements => elements, unused_size => base_font_size)
        end associate

    end function calculate_mathtext_width

    function calculate_mathtext_height(elements, base_font_size) result(total_height)
        !! Calculate total height of mathematical text elements
        !! This function signature is used by text_rendering but implementation moved there
        type(mathtext_element_t), intent(in) :: elements(:)
        real(wp), intent(in) :: base_font_size
        integer :: total_height

        ! This is a placeholder - actual implementation is in text_rendering module
        ! to avoid circular dependencies
        total_height = int(base_font_size)

        ! Suppress unused parameter warnings
        associate(unused_elements => elements, unused_size => base_font_size)
        end associate

    end function calculate_mathtext_height

    subroutine render_mathtext_elements(image_data, width, height, x, y, elements, &
                                       r, g, b, base_font_size)
        !! Render mathematical text elements to image
        !! This function signature is used by text_rendering but implementation moved there
        integer(1), intent(inout) :: image_data(*)
        integer, intent(in) :: width, height, x, y
        type(mathtext_element_t), intent(in) :: elements(:)
        integer(1), intent(in) :: r, g, b
        real(wp), intent(in) :: base_font_size

        ! This is a placeholder - actual implementation is in text_rendering module
        ! to avoid circular dependencies

        ! Suppress unused parameter warnings - avoid referencing assumed-size arrays
        if (.false.) then
            image_data(1) = image_data(1)  ! Reference array without bounds
        end if
        if (.false.) then
            associate(unused_w => width, unused_h => height, &
                     unused_x => x, unused_y => y, unused_elements => elements, &
                     unused_r => r, unused_g => g, unused_b => b, unused_size => base_font_size)
            end associate
        end if

    end subroutine render_mathtext_elements

end module fortplot_mathtext