submodule (fortplot_pdf) fortplot_pdf_draw !! PDF drawing, text rendering, fill operations, and markers !! !! Single Responsibility: Handle all drawing operations including lines, !! text, fills, markers, and arrows. use, intrinsic :: ieee_arithmetic, only: ieee_is_nan implicit none contains module subroutine draw_pdf_line(this, x1, y1, x2, y2) 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 module 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 module 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 module 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 module 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 module subroutine draw_pdf_text_styled(this, x_pt, y_pt, text, font_size, rotation, & ha, va, bbox, color) use fortplot_pdf_text_metrics, only: estimate_pdf_text_width class(pdf_context), intent(inout) :: this real(wp), intent(in) :: x_pt, y_pt character(len=*), intent(in) :: text real(wp), intent(in) :: font_size real(wp), intent(in) :: rotation character(len=*), intent(in) :: ha, va logical, intent(in) :: bbox real(wp), intent(in) :: color(3) real(wp) :: x0, y0 real(wp) :: w_pt, h_pt, pad real(wp) :: ascent_pt, descent_pt real(wp) :: baseline_pt, box_bottom_pt character(len=256) :: cmd w_pt = estimate_pdf_text_width(trim(text), font_size) h_pt = max(1.0_wp, 1.2_wp*font_size) ascent_pt = 0.8_wp*h_pt descent_pt = 0.2_wp*h_pt x0 = x_pt select case (trim(ha)) case ('center') x0 = x0-0.5_wp*w_pt case ('right') x0 = x0-w_pt case default end select ! Matplotlib semantics: the (x_pt, y_pt) anchor is the aligned bounding-box ! position, not the baseline. baseline_pt = y_pt box_bottom_pt = y_pt select case (trim(va)) case ('center') box_bottom_pt = y_pt-0.5_wp*h_pt baseline_pt = box_bottom_pt+descent_pt case ('top') box_bottom_pt = y_pt-h_pt baseline_pt = y_pt-ascent_pt case ('bottom') box_bottom_pt = y_pt baseline_pt = y_pt+descent_pt case default box_bottom_pt = y_pt baseline_pt = y_pt end select y0 = box_bottom_pt if (bbox) then pad = max(1.0_wp, 0.2_wp*font_size) call this%stream_writer%add_to_stream('q') call this%stream_writer%add_to_stream('1 1 1 rg') call this%stream_writer%add_to_stream('0 0 0 RG') call this%stream_writer%add_to_stream('0.5 w') write (cmd, '(F0.3,1X,F0.3,1X,F0.3,1X,F0.3," re B")') & x0-pad, y0-pad, w_pt+2.0_wp*pad, h_pt+2.0_wp*pad call this%stream_writer%add_to_stream(trim(cmd)) call this%stream_writer%add_to_stream('Q') end if call this%core_ctx%set_color(color(1), color(2), color(3)) if (abs(rotation) > 1.0e-6_wp) then call draw_rotated_mixed_font_text(this%core_ctx, x0, baseline_pt, & trim(text), & font_size, rotation) else call render_mixed_text(this%core_ctx, x0, baseline_pt, trim(text), & font_size) end if end subroutine draw_pdf_text_styled module 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-star 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-star to eliminate anti-aliasing gaps call this%stream_writer%add_to_stream('B') end if end subroutine fill_quad_wrapper module subroutine fill_heatmap_wrapper(this, x_grid, y_grid, z_grid, z_min, z_max, colormap_name) class(pdf_context), intent(inout) :: this real(wp), contiguous, intent(in) :: x_grid(:), y_grid(:), z_grid(:, :) real(wp), intent(in) :: z_min, z_max character(len=*), intent(in), optional :: colormap_name 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) if (present(colormap_name)) then call colormap_value_to_color(value, z_min, z_max, trim(colormap_name), color) else call colormap_value_to_color(value, z_min, z_max, 'viridis', color) end if 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 module 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 module 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 this%stream_writer%set_marker_gstate('') call pdf_set_marker_colors(this%stream_writer, edge_r, edge_g, edge_b, & face_r, face_g, face_b) end subroutine set_marker_colors_wrapper module 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 character(len=32) :: gstate_name call this%core_ctx%register_extgstate(edge_alpha, face_alpha, gstate_name) call pdf_set_marker_colors_with_alpha(this%stream_writer, edge_r, edge_g, & edge_b, edge_alpha, face_r, face_g, & face_b, face_alpha, gstate_name) end subroutine set_marker_colors_with_alpha_wrapper module 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 module 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 end submodule fortplot_pdf_draw