Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions src/backends/raster/fortplot_raster_primitives.f90
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,24 @@ subroutine blend_pixel(image_data, img_w, img_h, x, y, alpha, new_r, new_g, new_
real(wp) :: old_r, old_g, old_b, blend_r, blend_g, blend_b
real(wp) :: clamped_alpha

! Consistent coordinate rounding for line-marker alignment (Issue #333)
! Both line and marker drawing use nint() for identical pixel targeting
ix = nint(x)
iy = nint(y)

! Consistent coordinate rounding for line-marker alignment (Issue #333).
! Incoming coordinates use matplotlib's device convention: the image
! origin is top-left and the layout produces matplotlib-exact edges
! (e.g. the top spine at device y=58). In that convention device
! coordinate N is the centre of the 0-indexed display pixel N, which is
! the 1-based array element N+1. Mapping with a bare nint() biased the
! whole raster one display pixel inward (spines/ticks/curve all rendered
! one pixel low). The +1 restores matplotlib pixel registration.
ix = nint(x) + 1
iy = nint(y) + 1

! Bounds checking: Fortran uses 1-based indexing
if (ix < 1 .or. ix > img_w .or. iy < 1 .or. iy > img_h) return

! Clamp alpha to valid range and skip transparent pixels
clamped_alpha = max(0.0_wp, min(1.0_wp, alpha))
if (clamped_alpha < 1e-6_wp) return

! Calculate 1D array index for packed RGB data
! Layout: R1 G1 B1 R2 G2 B2 ... (row-major order)
idx = (iy - 1) * img_w * 3 + (ix - 1) * 3 + 1
Expand Down
14 changes: 9 additions & 5 deletions test/figure/test_suptitle.f90
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,16 @@ subroutine test_subplot_suptitle_layout(passed)

allocate (rgb(fig%get_width(), fig%get_height(), 3))
call fig%extract_rgb_data_for_animation(rgb)
top_dark = count_dark_pixels(rgb, 1, int(0.12_wp * real(size(rgb, 2), wp)))
! The top band spans the matplotlib top 12% of the figure. Pixel rows are
! 1-based here while the raster places features at matplotlib's 0-based
! device rows, so the band's last array row is int(0.12*h) + 1 to include
! the subplot top spine that sits on the 12% boundary.
top_dark = count_dark_pixels(rgb, 1, int(0.12_wp * real(size(rgb, 2), wp)) + 1)
deallocate (rgb)
! The suptitle glyphs alone deposit several hundred dark pixels in the
! top band; an empty band yields essentially none. The threshold detects
! the title's presence without depending on stroke width (raster strokes
! now render at the matplotlib 1.5pt footprint rather than ~2x wide).
! The subplot top spine and suptitle glyphs deposit several hundred dark
! pixels in the top band; an empty band yields essentially none. The
! threshold detects content presence without depending on stroke width
! (raster strokes now render at the matplotlib 1.5pt footprint).
if (top_dark >= 400) then
print *, ' PASS: 1x3 suptitle renders in the top band'
else
Expand Down
10 changes: 7 additions & 3 deletions test/line_style/test_dashdot_rendering.f90
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ subroutine test_dotted_has_substantial_gaps()

dark_pixels = 0
light_pixels = 0
! The line is drawn at device y=25; with matplotlib pixel registration
! it renders on the 0-indexed display row 25 (raster array row 26).
do i = 10, 390
idx = 1 + 24*img%width*3 + (i-1)*3
idx = 1 + 25*img%width*3 + (i-1)*3
px_val = iand(int(img%image_data(idx), int32), 255_int32)
if (px_val < 128_int32) then
dark_pixels = dark_pixels + 1
Expand Down Expand Up @@ -102,8 +104,9 @@ subroutine test_dashdot_has_gaps()
! than pure white; a pixel well above the drawn black (>100) marks a gap.
gap_pixels = 0
total_pixels = 0
! Line at device y=25 renders on 0-indexed display row 25 (array row 26).
do i = 10, 390
idx = 1 + 24*img%width*3 + (i-1)*3
idx = 1 + 25*img%width*3 + (i-1)*3
px_val = iand(int(img%image_data(idx), int32), 255_int32)
total_pixels = total_pixels + 1
if (px_val > 100_int32) gap_pixels = gap_pixels + 1
Expand Down Expand Up @@ -166,8 +169,9 @@ subroutine test_dashdot_vs_dotted_different()
dot_dark = 0
dd_dark = 0
patterns_differ = .false.
! Line at device y=25 renders on 0-indexed display row 25 (array row 26).
do i = 10, 390
idx = 1 + 24*img_dot%width*3 + (i-1)*3
idx = 1 + 25*img_dot%width*3 + (i-1)*3
px_dot = iand(int(img_dot%image_data(idx), int32), 255_int32)
px_dd = iand(int(img_dashdot%image_data(idx), int32), 255_int32)
if (px_dot < 128_int32) dot_dark = dot_dark + 1
Expand Down
Loading