fortplot_figure_core_io.f90 Source File


Source Code

module fortplot_figure_core_io
    !! Figure I/O and rendering operations module
    !!
    !! This module contains figure save/show/render functionality
    !! extracted from fortplot_figure_core for architectural compliance
    !!
    !! ARCHITECTURAL REFACTORING (Issue #678):
    !! - Focused module for I/O operations
    !! - Single Responsibility Principle compliance
    !! - Clean separation from plot management

    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_utils, only: get_backend_from_filename
    use fortplot_figure_initialization, only: figure_state_t
    use fortplot_figure_configuration, only: setup_figure_backend, &
        set_figure_labels, set_figure_scales
    use fortplot_errors, only: SUCCESS, ERROR_FILE_IO, is_error
    use fortplot_logging, only: log_error, log_warning
    use fortplot_png, only: png_context
    use fortplot_pdf, only: pdf_context
    use fortplot_ascii, only: ascii_context
    use fortplot_svg, only: svg_context
    use fortplot_figure_render_engine, only: figure_render
    use fortplot_figure_io, only: save_backend_with_status
    use fortplot_figure_utilities, only: is_interactive_environment, wait_for_user_input
    use fortplot_plot_data, only: plot_data_t, subplot_data_t
    implicit none

    private
    public :: savefig_figure, savefig_with_status_figure, show_figure, &
              render_figure_impl

contains

    subroutine savefig_figure(state, plots, plot_count, filename, blocking, &
                              annotations, annotation_count, &
                              subplots_array, subplot_rows, subplot_cols)
        !! Save figure to file (backward compatibility version)
        use fortplot_annotations, only: text_annotation_t
        type(figure_state_t), intent(inout) :: state
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(in) :: plot_count
        character(len=*), intent(in) :: filename
        logical, intent(in), optional :: blocking
        type(text_annotation_t), intent(in), optional :: annotations(:)
        integer, intent(in), optional :: annotation_count
        ! Optional subplot data for multi-axes rendering
        type(subplot_data_t), intent(in), optional :: subplots_array(:, :)
        integer, intent(in), optional :: subplot_rows, subplot_cols

        integer :: status

        ! Delegate to version with status reporting
        call savefig_with_status_figure(state, plots, plot_count, filename, status, &
                                        blocking, &
                                        annotations, annotation_count, subplots_array, &
                                        subplot_rows, subplot_cols)

        ! Log error if save failed (maintains existing behavior)
        if (status /= SUCCESS) then
            call log_error("Failed to save figure to '"//trim(filename)//"'")
        end if
    end subroutine savefig_figure

    subroutine savefig_with_status_figure(state, plots, plot_count, filename, status, &
                                          blocking, &
                                          annotations, annotation_count, &
                                          subplots_array, subplot_rows, &
                                          subplot_cols)
        !! Save figure to file with error status reporting
        !! Added Issue #854: File path validation for user input safety
        use fortplot_annotations, only: text_annotation_t
        use fortplot_parameter_validation, only: validate_file_path, &
                                                 parameter_validation_result_t
        type(figure_state_t), intent(inout) :: state
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(in) :: plot_count
        character(len=*), intent(in) :: filename
        integer, intent(out) :: status
        logical, intent(in), optional :: blocking
        type(text_annotation_t), intent(in), optional :: annotations(:)
        integer, intent(in), optional :: annotation_count
        ! Optional subplot data for multi-axes rendering
        type(subplot_data_t), intent(in), optional :: subplots_array(:, :)
        integer, intent(in), optional :: subplot_rows, subplot_cols

        character(len=20) :: required_backend, current_backend
        logical :: block, need_backend_switch
        type(parameter_validation_result_t) :: path_validation

        ! Initialize success status
        status = SUCCESS

        ! Validate filename path before proceeding
        path_validation = validate_file_path(filename, check_parent=.true., &
                                             context="savefig")
        if (.not. path_validation%is_valid) then
            status = ERROR_FILE_IO
            return
        end if

        block = .true.
        if (present(blocking)) block = blocking

        ! Determine required backend from filename extension
        required_backend = get_backend_from_filename(filename)

        ! Determine current backend type
        select type (backend => state%backend)
        type is (png_context)
            current_backend = 'png'
        type is (pdf_context)
            current_backend = 'pdf'
        type is (svg_context)
            current_backend = 'svg'
        type is (ascii_context)
            current_backend = 'ascii'
        class default
            current_backend = 'unknown'
        end select

        ! Check if we need to switch backends
        need_backend_switch = (trim(required_backend) /= trim(current_backend))

        if (need_backend_switch) then
            call setup_figure_backend(state, required_backend)
        else
            ! Clear ASCII canvas before re-rendering to prevent frame ghosting
            select type (backend => state%backend)
            type is (ascii_context)
                if (.not. state%rendered) then
                    backend%canvas = ' '
                    backend%num_text_elements = 0
                    backend%num_legend_lines = 0
                end if
            end select
        end if

        ! Render if not already rendered (with annotations if provided)
        if (.not. state%rendered) then
            call render_figure_impl(state, plots, plot_count, annotations, &
                                    annotation_count, &
                                    subplots_array, subplot_rows, subplot_cols)
        end if

        ! Save the figure with status checking
        call save_backend_with_status(state%backend, filename, status)
    end subroutine savefig_with_status_figure

    subroutine show_figure(state, plots, plot_count, blocking, annotations, &
                           annotation_count, &
                           subplots_array, subplot_rows, subplot_cols)
        !! Display the figure
        use fortplot_annotations, only: text_annotation_t
        type(figure_state_t), intent(inout) :: state
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(in) :: plot_count
        logical, intent(in), optional :: blocking
        type(text_annotation_t), intent(in), optional :: annotations(:)
        integer, intent(in), optional :: annotation_count
        ! Optional subplot data for multi-axes rendering
        type(subplot_data_t), intent(in), optional :: subplots_array(:, :)
        integer, intent(in), optional :: subplot_rows, subplot_cols

        logical :: block

        ! Default to non-blocking behavior to prevent hangs in automated environments
        ! Users can explicitly set blocking=true for interactive sessions
        block = .false.
        if (present(blocking)) block = blocking

        ! Render if not already rendered (with annotations if provided)
        if (.not. state%rendered) then
            call render_figure_impl(state, plots, plot_count, annotations, &
                                    annotation_count, &
                                    subplots_array, subplot_rows, subplot_cols)
        end if

        ! Display the figure
        call state%backend%save("terminal")

        ! Handle blocking behavior - when blocking=true, wait for user input
        if (block) then
            call wait_for_user_input()
        end if
    end subroutine show_figure

    subroutine render_figure_impl(state, plots, plot_count, annotations, &
                                  annotation_count, &
                                  subplots_array, subplot_rows, subplot_cols)
        !! Main rendering pipeline implementation
        !! Fixed Issue #432: Always render axes/labels even with no plot data
        !! Fixed Issue #844: ASCII annotation functionality
        use fortplot_annotations, only: text_annotation_t
        type(figure_state_t), intent(inout) :: state
        type(plot_data_t), intent(inout) :: plots(:)
        integer, intent(in) :: plot_count
        type(text_annotation_t), intent(in), optional :: annotations(:)
        integer, intent(in), optional :: annotation_count
        ! Optional subplot data for multi-axes rendering
        type(subplot_data_t), intent(in), optional :: subplots_array(:, :)
        integer, intent(in), optional :: subplot_rows, subplot_cols

        if (present(subplots_array) .and. present(subplot_rows) .and. &
            present(subplot_cols)) then
            call figure_render(state, plots, plot_count, annotations, &
                               annotation_count, &
                               subplots_array=subplots_array, &
                               subplot_rows=subplot_rows, &
                               subplot_cols=subplot_cols)
        else
            call figure_render(state, plots, plot_count, annotations, annotation_count)
        end if
    end subroutine render_figure_impl

end module fortplot_figure_core_io