fortplot_pdf.f90 Source File


Source Code

module fortplot_pdf
    !! PDF backend main interface (unified coordinates using plot area)

    use fortplot_pdf_core
    use fortplot_pdf_text
    use fortplot_pdf_drawing
    use fortplot_zlib_core, only: zlib_compress_into
    use fortplot_pdf_axes, only: draw_pdf_axes_and_labels, render_mixed_text
    use fortplot_pdf_io
    use fortplot_pdf_coordinate
    use fortplot_pdf_markers

    use fortplot_context, only: plot_context, setup_canvas
    use fortplot_plot_data, only: plot_data_t
    use fortplot_legend, only: legend_entry_t
    use fortplot_latex_parser, only: process_latex_in_text
    use fortplot_margins, only: plot_margins_t, plot_area_t, calculate_plot_area
    use fortplot_constants, only: EPSILON_COMPARE
    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_colormap, only: colormap_value_to_color
    use fortplot_logging, only: log_error, log_info
    implicit none

    private

    public :: pdf_context, create_pdf_canvas
    public :: draw_pdf_axes_and_labels, draw_mixed_font_text
    public :: pdf_stream_writer

    type, extends(plot_context) :: pdf_context
        type(pdf_stream_writer) :: stream_writer
        type(plot_margins_t) :: margins
        type(plot_area_t) :: plot_area
        type(pdf_context_core), private :: core_ctx
        type(pdf_context_handle), private :: coord_ctx
        integer :: x_tick_count = 0
        integer :: y_tick_count = 0
        logical, private :: axes_rendered = .false.
    contains
        procedure :: line => draw_pdf_line
        procedure :: color => set_pdf_color
        procedure :: text => draw_pdf_text_wrapper
        procedure :: save => write_pdf_file_facade
        procedure :: set_line_width => set_pdf_line_width
        procedure :: set_line_style => set_pdf_line_style
        procedure :: draw_marker => draw_pdf_marker_wrapper
        procedure :: set_marker_colors => set_marker_colors_wrapper
        procedure :: set_marker_colors_with_alpha => &
            set_marker_colors_with_alpha_wrapper
        procedure :: draw_arrow => draw_pdf_arrow_wrapper
        procedure :: get_ascii_output => pdf_get_ascii_output

        procedure :: get_width_scale => get_width_scale_wrapper
        procedure :: get_height_scale => get_height_scale_wrapper
        procedure :: fill_quad => fill_quad_wrapper
        procedure :: fill_heatmap => fill_heatmap_wrapper
        procedure :: render_legend_specialized => render_legend_specialized_wrapper
        procedure :: calculate_legend_dimensions => calculate_legend_dimensions_wrapper
        procedure :: set_legend_border_width => set_legend_border_width_wrapper
        procedure :: calculate_legend_position_backend => &
            calculate_legend_position_wrapper
        procedure :: extract_rgb_data => extract_rgb_data_wrapper
        procedure :: get_png_data_backend => get_png_data_wrapper
        procedure :: prepare_3d_data => prepare_3d_data_wrapper
        procedure :: render_ylabel => render_ylabel_wrapper
        procedure :: draw_axes_and_labels_backend => &
            draw_axes_and_labels_backend_wrapper
        procedure :: save_coordinates => pdf_save_coordinates
        procedure :: set_coordinates => pdf_set_coordinates
        procedure :: render_axes => render_pdf_axes_wrapper

        procedure, private :: update_coord_context
        procedure, private :: make_coord_context
    end type pdf_context

contains

    function create_pdf_canvas(width, height) result(ctx)
        integer, intent(in) :: width, height
        type(pdf_context) :: ctx
        ! Align PDF canvas size with matplotlib's inches/DPI semantics.
        ! Our figure dimensions are in pixels at a default DPI of 100.
        ! PDF units are points (1 pt = 1/72 inch). Convert pixels -> points
        ! so that an 800x600px figure maps to a 8x6 inch PDF page (576x432 pt).
        real(wp) :: width_pts, height_pts
        integer :: width_pts_i, height_pts_i

        call setup_canvas(ctx, width, height)

        width_pts = real(width, wp)*72.0_wp/100.0_wp
        height_pts = real(height, wp)*72.0_wp/100.0_wp
        ! Use integer canvas for downstream plot-area computations
        width_pts_i = max(1, nint(width_pts))
        height_pts_i = max(1, nint(height_pts))

        ctx%core_ctx = create_pdf_canvas_core(real(width_pts_i, wp), &
                                              real(height_pts_i, wp))

        call ctx%stream_writer%initialize_stream()
        call ctx%stream_writer%add_to_stream("q")
        call ctx%stream_writer%add_to_stream("1 w")
        call ctx%stream_writer%add_to_stream("1 J")
        call ctx%stream_writer%add_to_stream("1 j")
        call ctx%stream_writer%add_to_stream("0 0 0 RG")
        call ctx%stream_writer%add_to_stream("0 0 0 rg")

        ctx%margins = plot_margins_t()
        call calculate_pdf_plot_area(width_pts_i, height_pts_i, ctx%margins, &
                                     ctx%plot_area)

        call ctx%update_coord_context()
    end function create_pdf_canvas

    subroutine draw_pdf_line(this, x1, y1, x2, y2)
        use, intrinsic :: ieee_arithmetic, only: ieee_is_nan
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: x1, y1, x2, y2
        real(wp) :: pdf_x1, pdf_y1, pdf_x2, pdf_y2
        ! Ensure coordinate context reflects latest figure ranges and plot area
        call this%update_coord_context()

        ! Skip drawing if any coordinate is NaN (disconnected line segments)
        if (ieee_is_nan(x1) .or. ieee_is_nan(y1) .or. &
            ieee_is_nan(x2) .or. ieee_is_nan(y2)) then
            return
        end if

        call normalize_to_pdf_coords(this%coord_ctx, x1, y1, pdf_x1, pdf_y1)
        call normalize_to_pdf_coords(this%coord_ctx, x2, y2, pdf_x2, pdf_y2)
        call this%stream_writer%draw_vector_line(pdf_x1, pdf_y1, pdf_x2, pdf_y2)
    end subroutine draw_pdf_line

    subroutine set_pdf_color(this, r, g, b)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: r, g, b

        call this%stream_writer%set_vector_color(r, g, b)
        call this%core_ctx%set_color(r, g, b)
    end subroutine set_pdf_color

    subroutine set_pdf_line_width(this, width)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: width

        call this%stream_writer%set_vector_line_width(width)
        call this%core_ctx%set_line_width(width)
    end subroutine set_pdf_line_width

    subroutine set_pdf_line_style(this, style)
        class(pdf_context), intent(inout) :: this
        character(len=*), intent(in) :: style
        character(len=64) :: dash_pattern

        ! Convert line style to PDF dash pattern
        select case (trim(style))
        case ('-', 'solid')
            dash_pattern = '[] 0 d'  ! Solid line (empty dash array)
        case ('--', 'dashed')
            dash_pattern = '[6 3] 0 d'  ! 6 on, 3 off (approx. Matplotlib)
        case (':', 'dotted')
            dash_pattern = '[1 3] 0 d'  ! 1 on, 3 off
        case ('-.', 'dashdot')
            dash_pattern = '[6 3 1 3] 0 d'  ! dash-dot pattern
        case default
            dash_pattern = '[] 0 d'  ! Default to solid
        end select

        call this%stream_writer%add_to_stream(trim(dash_pattern))
    end subroutine set_pdf_line_style

    subroutine draw_pdf_text_wrapper(this, x, y, text)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: x, y
        character(len=*), intent(in) :: text
        real(wp) :: pdf_x, pdf_y

        ! Keep context in sync for text coordinate normalization
        call this%update_coord_context()
        call normalize_to_pdf_coords(this%coord_ctx, x, y, pdf_x, pdf_y)

        ! Use render_mixed_text which handles LaTeX processing and mathtext
        ! (superscripts/subscripts) properly, just like titles do
        call render_mixed_text(this%core_ctx, pdf_x, pdf_y, text)
    end subroutine draw_pdf_text_wrapper

    subroutine write_pdf_file_facade(this, filename)
        use fortplot_system_viewer, only: launch_system_viewer, &
                                          has_graphical_session, &
                                          get_temp_filename
        class(pdf_context), intent(inout) :: this
        character(len=*), intent(in) :: filename
        logical :: file_success
        character(len=1024) :: actual_filename
        logical :: viewer_success

        ! Handle terminal display
        if (trim(filename) == 'terminal') then
            if (has_graphical_session()) then
                call get_temp_filename('.pdf', actual_filename)
            else
                call log_info("No graphical session detected, cannot display PDF")
                call log_info("Use savefig('filename.pdf') to save to file or")
                call log_info("Use savefig('filename.txt') for ASCII rendering")
                return
            end if
        else
            actual_filename = filename
        end if

        ! Do not re-render axes here. The main rendering pipeline has already
        ! produced the complete `core_ctx%stream_data`, including axes, tick labels,
        ! titles/axis labels, legend text, and annotations. Re-rendering would
        ! clear or overwrite that state and can drop labels/legend.

        ! Merge vector drawing stream (lines, markers, etc.) with the core text
        ! stream. Keep existing `core_ctx%stream_data` intact to preserve labels
        ! and legend text that were rendered earlier in the pipeline.
        if (len_trim(this%stream_writer%content_stream) > 0) then
            if (len_trim(this%core_ctx%stream_data) > 0) then
                this%core_ctx%stream_data = trim(this%stream_writer%content_stream)//new_line('a')//trim(this%core_ctx%stream_data)
            else
                this%core_ctx%stream_data = this%stream_writer%content_stream
            end if
        end if

        ! Ensure a solid dash reset exists in the final content stream so that
        ! axes frame and tick marks are rendered with solid strokes regardless
        ! of prior plot linestyle state. This is harmless if plots later set a
        ! different dash pattern; the presence of this operator guarantees the
        ! PDF stream contains an explicit solid dash command.
        this%core_ctx%stream_data = &
            '[] 0 d'//new_line('a')//trim(this%core_ctx%stream_data)
        call write_pdf_file(this%core_ctx, actual_filename, file_success)
        if (.not. file_success) return

        ! Launch viewer if displaying to terminal
        if (trim(filename) == 'terminal' .and. has_graphical_session()) then
            call launch_system_viewer(actual_filename, viewer_success)
            if (.not. viewer_success) then
              call log_error("Failed to launch PDF viewer for: "//trim(actual_filename))
                call log_info("You can manually open: "//trim(actual_filename))
            end if
        end if
    end subroutine write_pdf_file_facade

    subroutine update_coord_context(this)
        class(pdf_context), intent(inout) :: this

        this%coord_ctx%x_min = this%x_min
        this%coord_ctx%x_max = this%x_max
        this%coord_ctx%y_min = this%y_min
        this%coord_ctx%y_max = this%y_max
        ! Coordinate context should operate in the same units as the PDF page
        ! dimensions (points). Keep plot area (already computed in points) and
        ! propagate the converted canvas size by recomputing from core context.
        this%coord_ctx%width = int(this%core_ctx%width)
        this%coord_ctx%height = int(this%core_ctx%height)
        this%coord_ctx%plot_area = this%plot_area
        this%coord_ctx%core_ctx = this%core_ctx
    end subroutine update_coord_context

    function make_coord_context(this) result(ctx)
        class(pdf_context), intent(in) :: this
        type(pdf_context_handle) :: ctx

        ctx%x_min = this%x_min
        ctx%x_max = this%x_max
        ctx%y_min = this%y_min
        ctx%y_max = this%y_max
        ctx%width = int(this%core_ctx%width)
        ctx%height = int(this%core_ctx%height)
        ctx%plot_area = this%plot_area
        ctx%core_ctx = this%core_ctx
    end function make_coord_context

    subroutine draw_pdf_marker_wrapper(this, x, y, style)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: x, y
        character(len=*), intent(in) :: style

        call this%update_coord_context()
        call draw_pdf_marker_at_coords(this%coord_ctx, this%stream_writer, x, y, style)
    end subroutine draw_pdf_marker_wrapper

    subroutine set_marker_colors_wrapper(this, edge_r, edge_g, edge_b, face_r, &
                                         face_g, face_b)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: edge_r, edge_g, edge_b, face_r, face_g, face_b

        call pdf_set_marker_colors(this%core_ctx, edge_r, edge_g, edge_b, face_r, &
                                   face_g, face_b)
    end subroutine set_marker_colors_wrapper

    subroutine set_marker_colors_with_alpha_wrapper(this, edge_r, edge_g, edge_b, &
                                                    edge_alpha, &
                                                    face_r, face_g, face_b, face_alpha)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: edge_r, edge_g, edge_b, edge_alpha
        real(wp), intent(in) :: face_r, face_g, face_b, face_alpha

        call pdf_set_marker_colors_with_alpha(this%core_ctx, edge_r, edge_g, edge_b, &
                                              edge_alpha, &
                                              face_r, face_g, face_b, face_alpha)
    end subroutine set_marker_colors_with_alpha_wrapper

    subroutine draw_pdf_arrow_wrapper(this, x, y, dx, dy, size, style)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: x, y, dx, dy, size
        character(len=*), intent(in) :: style

        call this%update_coord_context()
        call draw_pdf_arrow_at_coords(this%coord_ctx, this%stream_writer, x, y, dx, &
                                      dy, size, style)
    end subroutine draw_pdf_arrow_wrapper

    function pdf_get_ascii_output(this) result(output)
        class(pdf_context), intent(in) :: this
        character(len=:), allocatable :: output
        output = "PDF output (non-ASCII format)"
    end function pdf_get_ascii_output
    real(wp) function get_width_scale_wrapper(this) result(scale)
        class(pdf_context), intent(in) :: this
        type(pdf_context_handle) :: local_ctx
        local_ctx = this%make_coord_context()
        scale = pdf_get_width_scale(local_ctx)
    end function get_width_scale_wrapper
    real(wp) function get_height_scale_wrapper(this) result(scale)
        class(pdf_context), intent(in) :: this
        type(pdf_context_handle) :: local_ctx
        local_ctx = this%make_coord_context()
        scale = pdf_get_height_scale(local_ctx)
    end function get_height_scale_wrapper
    subroutine fill_quad_wrapper(this, x_quad, y_quad)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: x_quad(4), y_quad(4)
        real(wp) :: px(4), py(4)
        character(len=512) :: cmd
        integer :: i
        real(wp) :: minx, maxx, miny, maxy, eps

        call this%update_coord_context()

        ! Convert to PDF coordinates
        do i = 1, 4
            call normalize_to_pdf_coords(this%coord_ctx, x_quad(i), y_quad(i), &
                                         px(i), py(i))
        end do

        ! Check if quad is axis-aligned for potential optimization
        minx = min(min(px(1), px(2)), min(px(3), px(4)))
        maxx = max(max(px(1), px(2)), max(px(3), px(4)))
        miny = min(min(py(1), py(2)), min(py(3), py(4)))
        maxy = max(max(py(1), py(2)), max(py(3), py(4)))
        eps = 0.05_wp

        if ((abs(py(1) - py(2)) < 1.0e-6_wp .and. abs(px(2) - px(3)) < &
             1.0e-6_wp .and. &
             abs(py(3) - py(4)) < 1.0e-6_wp .and. abs(px(4) - px(1)) < &
             1.0e-6_wp)) then
            write (cmd, '(F0.3,1X,F0.3)') minx - eps, miny - eps; call this%stream_writer%add_to_stream(trim(cmd)//' m')
            write (cmd, '(F0.3,1X,F0.3)') maxx + eps, miny - eps; call this%stream_writer%add_to_stream(trim(cmd)//' l')
            write (cmd, '(F0.3,1X,F0.3)') maxx + eps, maxy + eps; call this%stream_writer%add_to_stream(trim(cmd)//' l')
            write (cmd, '(F0.3,1X,F0.3)') minx - eps, maxy + eps; call this%stream_writer%add_to_stream(trim(cmd)//' l')
            call this%stream_writer%add_to_stream('h')
            ! Use 'B' (fill and stroke) instead of 'f*' to eliminate anti-aliasing gaps
            call this%stream_writer%add_to_stream('B')
        else
            write (cmd, '(F0.3,1X,F0.3)') px(1), py(1); call this%stream_writer%add_to_stream(trim(cmd)//' m')
            write (cmd, '(F0.3,1X,F0.3)') px(2), py(2); call this%stream_writer%add_to_stream(trim(cmd)//' l')
            write (cmd, '(F0.3,1X,F0.3)') px(3), py(3); call this%stream_writer%add_to_stream(trim(cmd)//' l')
            write (cmd, '(F0.3,1X,F0.3)') px(4), py(4); call this%stream_writer%add_to_stream(trim(cmd)//' l')
            call this%stream_writer%add_to_stream('h')
            ! Use 'B' (fill and stroke) instead of 'f*' to eliminate anti-aliasing gaps
            call this%stream_writer%add_to_stream('B')
        end if
    end subroutine fill_quad_wrapper

    subroutine fill_heatmap_wrapper(this, x_grid, y_grid, z_grid, z_min, z_max)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: x_grid(:), y_grid(:), z_grid(:, :)
        real(wp), intent(in) :: z_min, z_max

        integer :: i, j, nx, ny, W, H
        real(wp) :: value
        real(wp), dimension(3) :: color
        integer :: idx
        integer :: out_len
        integer, allocatable :: rgb_u8(:)
        character(len=:), allocatable :: img_data
        real(wp) :: pdf_x0, pdf_y0, pdf_x1, pdf_y1, width_pt, height_pt
        real(wp) :: px_w, px_h, bleed_x, bleed_y
        character(len=256) :: cmd
        real(wp) :: v1, v2, v3

        call this%update_coord_context()

        nx = size(x_grid)
        ny = size(y_grid)

        ! Expect z_grid(ny, nx)
        if (size(z_grid, 1) /= ny .or. size(z_grid, 2) /= nx) return

        W = nx - 1; H = ny - 1
        if (W <= 0 .or. H <= 0) return

        ! Build RGB image with 1-pixel replicated border padding to avoid
        ! sampling outside the image at arbitrary zoom levels.
        block
            integer :: WP, HP
            integer, allocatable :: img(:, :, :)
            integer :: ii, jj, src_i, src_j
            WP = W + 2; HP = H + 2
            allocate (img(3, WP, HP))
            do jj = 1, HP
                do ii = 1, WP
                    src_i = max(1, min(W, ii - 1))
                    src_j = max(1, min(H, jj - 1))
                    value = z_grid(src_j, src_i)
                    call colormap_value_to_color(value, z_min, z_max, 'viridis', color)
                    v1 = max(0.0d0, min(1.0d0, color(1)))
                    v2 = max(0.0d0, min(1.0d0, color(2)))
                    v3 = max(0.0d0, min(1.0d0, color(3)))
                    img(1, ii, jj) = int(nint(v1*255.0d0), kind=4)
                    img(2, ii, jj) = int(nint(v2*255.0d0), kind=4)
                    img(3, ii, jj) = int(nint(v3*255.0d0), kind=4)
                end do
            end do
            allocate (rgb_u8(WP*HP*3))
            idx = 1
            do j = 1, HP
                do i = 1, WP
                    rgb_u8(idx) = img(1, i, j); idx = idx + 1
                    rgb_u8(idx) = img(2, i, j); idx = idx + 1
                    rgb_u8(idx) = img(3, i, j); idx = idx + 1
                end do
            end do
            W = WP; H = HP
        end block

        block
            use, intrinsic :: iso_fortran_env, only: int8
            integer(int8), allocatable :: in_bytes(:), out_bytes(:)
            integer :: k, n
            n = size(rgb_u8)
            allocate (in_bytes(n))
            do k = 1, n
                in_bytes(k) = int(iand(rgb_u8(k), 255))
            end do
            call zlib_compress_into(in_bytes, n, out_bytes, out_len)
            img_data = repeat(' ', out_len)
            do k = 1, out_len
                img_data(k:k) = achar(iand(int(out_bytes(k), kind=4), 255))
            end do
        end block

        ! Align placement to the exact PDF plot area (consistent with PNG backend)
        pdf_x0 = real(this%coord_ctx%plot_area%left, wp)
        pdf_y0 = real(this%coord_ctx%plot_area%bottom, wp)
        width_pt = real(this%coord_ctx%plot_area%width, wp)
        height_pt = real(this%coord_ctx%plot_area%height, wp)

        ! Compute a half-pixel bleed in user-space and clip to the exact plot area
        px_w = width_pt/real(W, wp)
        px_h = height_pt/real(H, wp)
        bleed_x = 0.5_wp*px_w
        bleed_y = 0.5_wp*px_h

        call this%stream_writer%add_to_stream('q')
        ! Clip to the exact target rectangle to keep padded borders inside
        write (cmd, '(F0.12,1X,F0.12,1X,F0.12,1X,F0.12,1X,A)') pdf_x0, pdf_y0, &
            width_pt, height_pt, ' re W n'
        call this%stream_writer%add_to_stream(trim(cmd))
        ! Compute pixel scale and place padded image so that the extra 1px ring
        ! lies just outside the clip region
        px_w = width_pt/real(W - 2, wp)
        px_h = height_pt/real(H - 2, wp)
        write (cmd, '(F0.12,1X,F0.12,1X,F0.12,1X,F0.12,1X,F0.12,1X,F0.12,1X,A)') &
            px_w*real(W, wp), 0.0_wp, 0.0_wp, -(px_h*real(H, wp)), &
            pdf_x0 - px_w, (pdf_y0 + height_pt) + px_h, ' cm'
        call this%stream_writer%add_to_stream(trim(cmd))
        ! Place image XObject instead of inline image
        call this%core_ctx%set_image(W, H, img_data)
        call this%stream_writer%add_to_stream('/Im1 Do')
        call this%stream_writer%add_to_stream('Q')
    end subroutine fill_heatmap_wrapper
    subroutine render_legend_specialized_wrapper(this, entries, x, y, width, height)
        class(pdf_context), intent(inout) :: this
        type(legend_entry_t), dimension(:), intent(in) :: entries
        real(wp), intent(in) :: x, y, width, height

        call this%update_coord_context()
        call pdf_render_legend_specialized(this%coord_ctx, entries, x, y, &
                                           width, height)
    end subroutine render_legend_specialized_wrapper

    subroutine calculate_legend_dimensions_wrapper(this, entries, width, height)
        class(pdf_context), intent(in) :: this
        type(legend_entry_t), dimension(:), intent(in) :: entries
        real(wp), intent(out) :: width, height
        type(pdf_context_handle) :: local_ctx

        local_ctx = this%make_coord_context()
        call pdf_calculate_legend_dimensions(local_ctx, entries, width, height)
    end subroutine calculate_legend_dimensions_wrapper

    subroutine set_legend_border_width_wrapper(this, width)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: width

        call this%update_coord_context()
        call pdf_set_legend_border_width(this%coord_ctx, width)
    end subroutine set_legend_border_width_wrapper

    subroutine calculate_legend_position_wrapper(this, loc, x, y)
        class(pdf_context), intent(in) :: this
        character(len=*), intent(in) :: loc
        real(wp), intent(out) :: x, y
        type(pdf_context_handle) :: local_ctx

        local_ctx = this%make_coord_context()
        call pdf_calculate_legend_position(local_ctx, loc, x, y)
    end subroutine calculate_legend_position_wrapper

    subroutine extract_rgb_data_wrapper(this, width, height, rgb_data)
        class(pdf_context), intent(in) :: this
        integer, intent(in) :: width, height
        real(wp), intent(out) :: rgb_data(width, height, 3)
        type(pdf_context_handle) :: local_ctx

        local_ctx = this%make_coord_context()
        call pdf_extract_rgb_data(local_ctx, width, height, rgb_data)
    end subroutine extract_rgb_data_wrapper

    subroutine get_png_data_wrapper(this, width, height, png_data, status)
        class(pdf_context), intent(in) :: this
        integer, intent(in) :: width, height
        integer(1), allocatable, intent(out) :: png_data(:)
        integer, intent(out) :: status
        type(pdf_context_handle) :: local_ctx

        local_ctx = this%make_coord_context()
        call pdf_get_png_data(local_ctx, width, height, png_data, status)
    end subroutine get_png_data_wrapper

    subroutine prepare_3d_data_wrapper(this, plots)
        class(pdf_context), intent(inout) :: this
        type(plot_data_t), intent(in) :: plots(:)

        call this%update_coord_context()
        call pdf_prepare_3d_data(this%coord_ctx, plots)
    end subroutine prepare_3d_data_wrapper

    subroutine render_ylabel_wrapper(this, ylabel)
        class(pdf_context), intent(inout) :: this
        character(len=*), intent(in) :: ylabel

        call this%update_coord_context()
        call pdf_render_ylabel(this%coord_ctx, ylabel)
    end subroutine render_ylabel_wrapper

    subroutine draw_axes_and_labels_backend_wrapper(this, xscale, yscale, &
                                                    symlog_threshold, &
                                                    x_min, x_max, y_min, y_max, &
                                                    title, xlabel, ylabel, &
                                                    z_min, z_max, has_3d_plots)
        use fortplot_3d_axes, only: draw_3d_axes
        use fortplot_pdf_axes, only: draw_pdf_title_and_labels
        class(pdf_context), intent(inout) :: this
        character(len=*), intent(in) :: xscale, yscale
        real(wp), intent(in) :: symlog_threshold
        real(wp), intent(in) :: x_min, x_max, y_min, y_max
        character(len=:), allocatable, intent(in), optional :: title, xlabel, ylabel
        real(wp), intent(in), optional :: z_min, z_max
        logical, intent(in) :: has_3d_plots

        character(len=256) :: title_str, xlabel_str, ylabel_str
        associate (dzmin => z_min, dzmax => z_max, dh3d => has_3d_plots); end associate

        title_str = ""; xlabel_str = ""; ylabel_str = ""
        if (present(title)) title_str = title
        if (present(xlabel)) xlabel_str = xlabel
        if (present(ylabel)) ylabel_str = ylabel

        if (has_3d_plots) then
            call draw_3d_axes(this, x_min, x_max, y_min, y_max, &
                              merge(z_min, 0.0_wp, present(z_min)), &
                              merge(z_max, 1.0_wp, present(z_max)))
            ! Draw only title/xlabel/ylabel using PDF helpers (avoid 2D axes duplication)
            call draw_pdf_title_and_labels(this%core_ctx, title_str, xlabel_str, &
                                           ylabel_str, &
                                           real(this%plot_area%left, wp), &
                                           real(this%plot_area%bottom, wp), &
                                           real(this%plot_area%width, wp), &
                                           real(this%plot_area%height, wp))
        else
            call draw_pdf_axes_and_labels(this%core_ctx, xscale, yscale, &
                                          symlog_threshold, &
                                          x_min, x_max, y_min, y_max, &
                                          title_str, xlabel_str, ylabel_str, &
                                          real(this%plot_area%left, wp), &
                                          real(this%plot_area%bottom, wp), &
                                          real(this%plot_area%width, wp), &
                                          real(this%plot_area%height, wp), &
                                          real(this%height, wp))
        end if
    end subroutine draw_axes_and_labels_backend_wrapper

    subroutine pdf_save_coordinates(this, x_min, x_max, y_min, y_max)
        class(pdf_context), intent(in) :: this
        real(wp), intent(out) :: x_min, x_max, y_min, y_max

        ! Return current coordinate bounds
        x_min = this%x_min
        x_max = this%x_max
        y_min = this%y_min
        y_max = this%y_max
    end subroutine pdf_save_coordinates

    subroutine pdf_set_coordinates(this, x_min, x_max, y_min, y_max)
        class(pdf_context), intent(inout) :: this
        real(wp), intent(in) :: x_min, x_max, y_min, y_max

        this%x_min = x_min
        this%x_max = x_max
        this%y_min = y_min
        this%y_max = y_max

        ! Reset axes flag when coordinates change
        this%axes_rendered = .false.
    end subroutine pdf_set_coordinates

    subroutine render_pdf_axes_wrapper(this, title_text, xlabel_text, ylabel_text)
        !! Explicitly render axes with optional labels
        !! This allows low-level PDF users to add proper axes to their plots
        class(pdf_context), intent(inout) :: this
        character(len=*), intent(in), optional :: title_text, xlabel_text, ylabel_text

        character(len=256) :: title_str, xlabel_str, ylabel_str

        ! Only render axes once unless coordinates change
        if (this%axes_rendered) return

        ! Ensure coordinate system is set
        if (abs(this%x_max - this%x_min) <= epsilon(1.0_wp) .or. &
            abs(this%y_max - this%y_min) <= epsilon(1.0_wp)) then
            ! No valid coordinate system - skip axes
            return
        end if

        ! Set default empty strings for labels
        title_str = ""
        xlabel_str = ""
        ylabel_str = ""

        ! Use provided labels if present
        if (present(title_text)) title_str = title_text
        if (present(xlabel_text)) xlabel_str = xlabel_text
        if (present(ylabel_text)) ylabel_str = ylabel_text

        ! Clear any previous axes data in core context
        this%core_ctx%stream_data = ""

        ! Draw axes and labels with current coordinate system
        call draw_pdf_axes_and_labels(this%core_ctx, "linear", "linear", 1.0_wp, &
                                      this%x_min, this%x_max, this%y_min, this%y_max, &
                                      title_str, xlabel_str, ylabel_str, &
                                      real(this%plot_area%left, wp), &
                                      real(this%plot_area%bottom, wp), &
                                      real(this%plot_area%width, wp), &
                                      real(this%plot_area%height, wp), &
                                      real(this%height, wp))

        ! Add axes content to the stream
        call this%stream_writer%add_to_stream(this%core_ctx%stream_data)

        ! Mark axes as rendered
        this%axes_rendered = .true.
    end subroutine render_pdf_axes_wrapper

end module fortplot_pdf