fortplot_raster_text_rendering.f90 Source File


Source Code

module fortplot_raster_text_rendering
    !! Raster-specific text rendering primitives (glyph rasterization, mathtext drawing)
    use iso_c_binding, only: c_ptr, c_f_pointer, c_int8_t, &
                                 c_associated
    use fortplot_stb_truetype
    use fortplot_unicode, only: utf8_to_codepoint, utf8_char_length
    use fortplot_text_fonts, only: init_text_system, get_global_font, get_font_scale, &
                                  is_font_initialized, get_font_scale_for_size, &
                                  get_font_metrics
    use fortplot_mathtext, only: parse_mathtext, mathtext_element_t
    use fortplot_raster_primitives, only: draw_line_distance_aa
    use fortplot_text_layout, only: has_mathtext, preprocess_math_text, calculate_mathtext_width_internal, &
                                    calculate_text_width_with_size_internal, &
                                    calculate_text_height_with_size_internal, &
                                    DEFAULT_FONT_SIZE
    use, intrinsic :: iso_fortran_env, only: wp => real64
    implicit none

    private
    public :: render_text_to_image, render_text_with_size, render_rotated_text_to_image

    real(wp), parameter :: PI = 3.14159265359_wp

contains

    subroutine render_text_to_image(image_data, width, height, x, y, text, r, g, b)
        !! Render text to image using STB TrueType with UTF-8 support
        !! Supports mathematical notation with superscripts and subscripts
        integer(1), intent(inout) :: image_data(:)
        integer, intent(in) :: width, height, x, y
        character(len=*), intent(in) :: text
        integer(1), intent(in) :: r, g, b
        integer :: pen_x, pen_y, i, char_code
        integer :: advance_width, left_side_bearing
        type(c_ptr) :: bitmap_ptr
        integer :: bmp_width, bmp_height, xoff, yoff
        integer :: char_len
        type(stb_fontinfo_t) :: font
        real(wp) :: scale
        type(mathtext_element_t), allocatable :: elements(:)
        character(len=2048) :: processed
        integer :: plen

        if (.not. is_font_initialized()) then
            if (.not. init_text_system()) then
                call render_simple_placeholder(image_data, width, height, x, y, r, g, b)
                return
            end if
        end if

        if (has_mathtext(text)) then
            call preprocess_math_text(text, processed, plen)
            elements = parse_mathtext(processed(1:plen))
            call render_mathtext_elements_internal(image_data, width, height, x, y, &
                elements, r, g, b, real(DEFAULT_FONT_SIZE, wp))
            return
        end if

        font = get_global_font()
        scale = get_font_scale()

        pen_x = x
        pen_y = y

        i = 1
        do while (i <= len_trim(text))
            char_len = utf8_char_length(text(i:i))
            if (char_len == 0) then
                char_code = iachar(text(i:i))
                i = i + 1
            else
                char_code = utf8_to_codepoint(text, i)
                i = i + char_len
            end if

            bitmap_ptr = stb_get_codepoint_bitmap(font, scale, scale, char_code, &
                                                 bmp_width, bmp_height, xoff, yoff)

            if (c_associated(bitmap_ptr)) then
                call render_stb_glyph(image_data, width, height, pen_x, pen_y, &
                    bitmap_ptr, bmp_width, bmp_height, xoff, yoff, r, g, &
                                     b)
                call stb_free_bitmap(bitmap_ptr)
            end if

            call stb_get_codepoint_hmetrics(font, char_code, advance_width, &
                left_side_bearing)
            pen_x = pen_x + int(real(advance_width) * scale)
        end do
    end subroutine render_text_to_image

    subroutine render_text_with_size(image_data, width, height, x, y, text, &
            r, g, b, pixel_height)
        !! Render text with specific font size
        !! Supports mathematical notation with superscripts and subscripts
        integer(1), intent(inout) :: image_data(:)
        integer, intent(in) :: width, height, x, y
        character(len=*), intent(in) :: text
        integer(1), intent(in) :: r, g, b
        real(wp), intent(in) :: pixel_height
        integer :: pen_x, pen_y, i, char_code
        integer :: advance_width, left_side_bearing
        type(c_ptr) :: bitmap_ptr
        integer :: bmp_width, bmp_height, xoff, yoff
        integer :: char_len
        type(stb_fontinfo_t) :: font
        real(wp) :: scale
        type(mathtext_element_t), allocatable :: elements(:)
        character(len=2048) :: processed
        integer :: plen

        if (.not. is_font_initialized()) then
            if (.not. init_text_system()) then
                return
            end if
        end if

        if (has_mathtext(text)) then
            call preprocess_math_text(text, processed, plen)
            elements = parse_mathtext(processed(1:plen))
            call render_mathtext_elements_internal(image_data, width, height, x, y, &
                elements, r, g, b, pixel_height)
            return
        end if

        font = get_global_font()
        scale = get_font_scale_for_size(pixel_height)

        pen_x = x
        pen_y = y

        i = 1
        do while (i <= len_trim(text))
            char_len = utf8_char_length(text(i:i))
            if (char_len == 0) then
                char_code = iachar(text(i:i))
                i = i + 1
            else
                char_code = utf8_to_codepoint(text, i)
                i = i + char_len
            end if

            bitmap_ptr = stb_get_codepoint_bitmap(font, scale, scale, char_code, &
                                                 bmp_width, bmp_height, xoff, yoff)

            if (c_associated(bitmap_ptr)) then
                call render_stb_glyph(image_data, width, height, pen_x, pen_y, &
                    bitmap_ptr, bmp_width, bmp_height, xoff, yoff, r, g, &
                    b)
                call stb_free_bitmap(bitmap_ptr)
            end if

            call stb_get_codepoint_hmetrics(font, char_code, advance_width, &
                left_side_bearing)
            pen_x = pen_x + int(real(advance_width) * scale)
        end do
    end subroutine render_text_with_size

    subroutine render_rotated_text_to_image(image_data, width, height, x, y, text, &
            r, g, b, angle)
        !! Render rotated text to PNG image using STB TrueType with UTF-8 support
        integer(1), intent(inout) :: image_data(:)
        integer, intent(in) :: width, height, x, y
        character(len=*), intent(in) :: text
        integer(1), intent(in) :: r, g, b
        real(wp), intent(in) :: angle  ! Rotation angle in degrees

        integer :: i, char_code, pen_x, pen_y
        integer :: advance_width, left_side_bearing
        type(c_ptr) :: bitmap_ptr
        integer :: bmp_width, bmp_height, xoff, yoff
        real(wp) :: cos_a, sin_a
        integer :: char_len
        type(stb_fontinfo_t) :: font
        real(wp) :: scale

        if (.not. is_font_initialized()) then
            if (.not. init_text_system()) then
                return
            end if
        end if

        font = get_global_font()
        scale = get_font_scale()

        pen_x = x
        pen_y = y
        cos_a = cos(angle * PI / 180.0_wp)
        sin_a = sin(angle * PI / 180.0_wp)

        i = 1
        do while (i <= len_trim(text))
            char_len = utf8_char_length(text(i:i))
            if (char_len == 0) then
                char_code = iachar(text(i:i))
                i = i + 1
            else
                char_code = utf8_to_codepoint(text, i)
                i = i + char_len
            end if

            bitmap_ptr = stb_get_codepoint_bitmap(font, scale, scale, char_code, &
                                                 bmp_width, bmp_height, xoff, yoff)

            if (c_associated(bitmap_ptr)) then
                call render_stb_glyph(image_data, width, height, pen_x, pen_y, &
                    bitmap_ptr, bmp_width, bmp_height, xoff, yoff, r, g, &
                                     b)
                call stb_free_bitmap(bitmap_ptr)
            end if

            call stb_get_codepoint_hmetrics(font, char_code, advance_width, &
                left_side_bearing)
            pen_x = pen_x + int(real(advance_width) * scale * cos_a)
            pen_y = pen_y + int(real(advance_width) * scale * sin_a)
        end do
    end subroutine render_rotated_text_to_image

    recursive subroutine render_mathtext_elements_internal(image_data, width, &
            height, x, y, elements, r, g, b, base_font_size)
        !! Render mathematical text elements to image for the raster backend
        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

        integer :: i, pen_x, pen_y
        real(wp) :: element_font_size
        integer :: rad_width, sym_w, rad_height, top_y
        real(wp) :: ascent, descent, line_gap
        logical :: success

        pen_x = x
        pen_y = y

        call get_font_metrics(ascent, descent, line_gap, success)
        if (.not. success) return

        do i = 1, size(elements)
            element_font_size = base_font_size * elements(i)%font_size_ratio

            if (elements(i)%element_type == 3) then
                pen_y = y

                rad_width = calculate_mathtext_width_internal(&
                    parse_mathtext(elements(i)%text), element_font_size)
                rad_height = calculate_text_height_with_size_internal(element_font_size)
                sym_w = int(0.6_wp * element_font_size)
                top_y = pen_y - rad_height
                call draw_line_distance_aa(image_data, width, height, &
                    real(pen_x, wp), real(pen_y, wp), real(pen_x + sym_w/2, wp), &
                    real(pen_y + sym_w/2, wp), real(r, wp)/255.0_wp, &
                    real(g, wp)/255.0_wp, real(b, wp)/255.0_wp, 0.1_wp)
                call draw_line_distance_aa(image_data, width, height, &
                    real(pen_x + sym_w/2, wp), real(pen_y + sym_w/2, wp), &
                    real(pen_x + sym_w, wp), real(top_y, wp), real(r, wp)/255.0_wp, &
                    real(g, wp)/255.0_wp, real(b, wp)/255.0_wp, 0.1_wp)
                call draw_line_distance_aa(image_data, width, height, &
                    real(pen_x + sym_w, wp), real(top_y, wp), &
                    real(pen_x + sym_w + rad_width, wp), real(top_y, wp), &
                    real(r, wp)/255.0_wp, real(g, wp)/255.0_wp, real(b, wp)/255.0_wp, &
                    0.1_wp)
                call render_mathtext_elements_internal(image_data, width, height, &
                    pen_x + sym_w, pen_y, parse_mathtext(elements(i)%text), r, g, &
                    b, element_font_size)
                pen_x = pen_x + sym_w + rad_width
            else
                pen_y = y - int(elements(i)%vertical_offset * base_font_size)
                call render_text_with_size_internal(image_data, width, height, &
                    pen_x, pen_y, elements(i)%text, r, g, b, element_font_size)
                pen_x = pen_x + calculate_text_width_with_size_internal(&
                    elements(i)%text, element_font_size)
            end if
        end do
    end subroutine render_mathtext_elements_internal

    subroutine render_text_with_size_internal(image_data, width, height, x, y, text, &
            r, g, b, pixel_height)
        !! Internal text rendering helper to avoid circular dependencies
        integer(1), intent(inout) :: image_data(:)
        integer, intent(in) :: width, height, x, y
        character(len=*), intent(in) :: text
        integer(1), intent(in) :: r, g, b
        real(wp), intent(in) :: pixel_height
        integer :: pen_x, pen_y, i, char_code
        integer :: advance_width, left_side_bearing
        type(c_ptr) :: bitmap_ptr
        integer :: bmp_width, bmp_height, xoff, yoff
        integer :: char_len
        type(stb_fontinfo_t) :: font
        real(wp) :: scale

        if (.not. is_font_initialized()) then
            if (.not. init_text_system()) then
                return
            end if
        end if

        font = get_global_font()
        scale = get_font_scale_for_size(pixel_height)

        pen_x = x
        pen_y = y

        i = 1
        do while (i <= len_trim(text))
            char_len = utf8_char_length(text(i:i))
            if (char_len == 0) then
                char_code = iachar(text(i:i))
                i = i + 1
            else
                char_code = utf8_to_codepoint(text, i)
                i = i + char_len
            end if

            bitmap_ptr = stb_get_codepoint_bitmap(font, scale, scale, char_code, &
                                                 bmp_width, bmp_height, xoff, yoff)

            if (c_associated(bitmap_ptr)) then
                call render_stb_glyph(image_data, width, height, pen_x, pen_y, &
                    bitmap_ptr, bmp_width, bmp_height, xoff, yoff, r, g, &
                    b)
                call stb_free_bitmap(bitmap_ptr)
            end if

            call stb_get_codepoint_hmetrics(font, char_code, advance_width, &
                left_side_bearing)
            pen_x = pen_x + int(real(advance_width) * scale)
        end do
    end subroutine render_text_with_size_internal

    subroutine render_stb_glyph(image_data, width, height, pen_x, pen_y, bitmap_ptr, &
                                bmp_width, bmp_height, xoff, yoff, r, g, b)
        !! Render STB TrueType glyph bitmap to image
        integer(1), intent(inout) :: image_data(:)
        integer, intent(in) :: width, height, pen_x, pen_y
        type(c_ptr), intent(in) :: bitmap_ptr
        integer, intent(in) :: bmp_width, bmp_height, xoff, yoff
        integer(1), intent(in) :: r, g, b
        integer(c_int8_t), pointer :: bitmap_buffer(:)
        integer :: glyph_x, glyph_y, img_x, img_y, row, col, pixel_idx
        integer :: alpha_int
        real :: alpha_f, bg_r, bg_g, bg_b

        if (bmp_width <= 0 .or. bmp_height <= 0) then
            return
        end if

        call c_f_pointer(bitmap_ptr, bitmap_buffer, [bmp_width * bmp_height])

        glyph_x = pen_x + xoff
        glyph_y = pen_y + yoff

        do row = 0, bmp_height - 1
            do col = 0, bmp_width - 1
                img_x = glyph_x + col
                img_y = glyph_y + row

                if (img_x >= 0 .and. img_x < width .and. img_y >= 0 .and. &
                    img_y < height) then
                    alpha_int = int(bitmap_buffer(row * bmp_width + col + 1))
                    if (alpha_int < 0) alpha_int = alpha_int + 256

                    if (alpha_int > 0) then
                        pixel_idx = (img_y * width + img_x) * 3 + 1

                        if (pixel_idx < 1 .or. pixel_idx + 2 > width * height * 3) then
                            cycle
                        end if

                        alpha_f = real(alpha_int) / 255.0
                        bg_r = real(int(image_data(pixel_idx), &
                            kind=selected_int_kind(2)) + &
                            merge(256, 0, image_data(pixel_idx) < 0))
                        bg_g = real(int(image_data(pixel_idx + 1), &
                            kind=selected_int_kind(2)) + &
                            merge(256, 0, image_data(pixel_idx + 1) < 0))
                        bg_b = real(int(image_data(pixel_idx + 2), &
                            kind=selected_int_kind(2)) + &
                            merge(256, 0, image_data(pixel_idx + 2) < 0))

                        image_data(pixel_idx) = int(bg_r * (1.0 - alpha_f) + &
                            real(int(r) + merge(256, 0, r < 0)) * alpha_f, 1)
                        image_data(pixel_idx + 1) = int(bg_g * (1.0 - alpha_f) + &
                            real(int(g) + merge(256, 0, g < 0)) * alpha_f, 1)
                        image_data(pixel_idx + 2) = int(bg_b * (1.0 - alpha_f) + &
                            real(int(b) + merge(256, 0, b < 0)) * alpha_f, 1)
                    end if
                end if
            end do
        end do
    end subroutine render_stb_glyph

    subroutine render_simple_placeholder(image_data, width, height, x, y, r, g, b)
        integer(1), intent(inout) :: image_data(:)
        integer, intent(in) :: width, height, x, y
        integer(1), intent(in) :: r, g, b
        integer :: pixel_idx, img_x, img_y, max_idx

        max_idx = width * height * 3

        do img_y = y, min(y + 6, height - 1)
            do img_x = x, min(x + 4, width - 1)
                if (img_x >= 0 .and. img_y >= 0) then
                    pixel_idx = (img_y * width + img_x) * 3 + 1
                    if (pixel_idx > 0 .and. pixel_idx <= max_idx - 2) then
                        image_data(pixel_idx) = r
                        image_data(pixel_idx + 1) = g
                        image_data(pixel_idx + 2) = b
                    end if
                end if
            end do
        end do
    end subroutine render_simple_placeholder

end module fortplot_raster_text_rendering