FCL-Image fails to draw ellipses having pen widths greater than 1
Summary
The canvas of FCL-Image is not able to draw ellipses correctly when the pen width is greater than 1. This is surprising because TFPPixelCanvas.DoEllipse
calls a special routine with a Width
parameter for this case:
with pen do
...
if pen.width > 1 then
DrawSolidEllipse (self, Bounds, width, FPColor)
else
DrawSolidEllipse (self, Bounds, FPColor);
System Information
- Operating system: Windows 11
- Processor architecture: x86-64
- Compiler version: 3.2.2, but also main (c7275d0b)
- Device: Computer
Steps to reproduce
Run the attached demo project. It is a Lazarus project (sorry, but I wanted to compare with the ellipses drawn by the Graphics unit) which draws three ellipses in the left paintbox by the Graphics units, as well as the same ellipses in the right paintbox into a bitmap painted by fcl-image. The scrollbar below the two images allows to vary the pen width.
- Pen.Width = 1: both images are roughly the same (except for some missing pixels at the sides of the top/left flat fcl-image ellipse)
- Pen.Width = 2: in the fcl-image box the circular ellipse is drawn with pen.width=1, the flat ellipse is not drawn at all except for a few pixels at the edge, and the narrow ellipse has varying pen width along the perimeter.
Pen width 1 | Pen width 2 |
---|---|
![]() |
![]() |
Example Project
What is the current bug behavior?
See description and screenshots above.
What is the expected (correct) behavior?
The fcl-image ellipses should look like the ellipses drawn by the LCL graphics unit (at least roughly)
Analysis
The pixels forming the ellipse border are calculated by the GatherEllipseInfo()
method of the TEllipseInfo
helper class. In case of Pen.Width > 1
borders for the outer and the inner border pixels are calculated in DrawSolidEllipse()
separately and stored in the infoOut
and infoIn
instances of TEllipseInfo
:
procedure DrawSolidEllipse (Canv:TFPCustomCanvas; const Bounds:TRect; Width:integer; const c:TFPColor);
var infoOut, infoIn : TEllipseInfo;
begin
...
infoOut.GatherEllipseInfo(bounds);
with bounds do
infoIn.GatherEllipseInfo (Rect(left+width,top+width,right-width,bottom-width));
In infoIn.GatherEllipseInfo
, the parameter width
is meant here as the Width
parameter provided as argument to the DrawSolidEllipse
procedure. bounds
is the rectangle with encloses the ellipse. Since the introduction of record helpers will not work any more because width
is also a method of TRect
- and this is active here because of the with bounds
instruction. A nice example for problems caused by with
...
Fix #1
Removing the with
instruction fixes the issue:
procedure DrawSolidEllipse (Canv:TFPCustomCanvas; const Bounds:TRect; Width:integer; const c:TFPColor);
var infoOut, infoIn : TEllipseInfo;
rct: TRect;
...
begin
dec(Width);
infoOut.GatherEllipseInfo(bounds);
rct := bounds;
rct.Inflate(-Width, -Width);
infoIn.GatherEllipseInfo(rct);
After this change the output of the above-mentioned demo looks like this:
Pen width 1 | Pen width 2 |
---|---|
![]() |
![]() |
Better, but not perfect: The border of the flat ellipse with pen width 2 looks thinner at the left and right edges, corresponding to the places where pixels are missing for the case of pen width = 1.
Fix #2
This is caused by a strange treatment of special cases in calculating the ellipse border pixels in TEllipseInfo.GatherEllipseInfo
:
with infoP^ do // ensure single width
begin
if r < halfnumber then
begin
if ytopmin = yt then
begin
inc (ytopmin);
dec (ybotmax);
end;
end
else
begin
if (ytopmax = pPy) and (ytopmax <> ytopmin) then
begin
dec (ytopmax);
inc (ybotmin);
end;
end;
pPy := ytopmin;
end;
To be honest, I don't understand what is happening in this "ensure single width" code, but when I remove these lines (as well as the same block found at another place), the ellipses look correct and very similar to those drawn by the LCL graphics unit:
Pen width 1 | Pen width 2 |
---|---|
![]() |
![]() |
Patch
This is a patch containing both modifications to fix the issue: 41286-ellipse_penwidth-v2.diff
In contrast to what was shown above in "Fix #1" the patch also takes care of centering the ellipse border in the bounds rectangle. The old code had the outer border touching the bounds rectangle, and the inner border shrunk the bounds by the pen width - this made the inner open ellipse area shrink in comparison to the LCL graphics case in particular when very thick pens are used. The new code moves the outer border outwards by half the pen width and shrinks the inner border again by the full pen width. Now the ellipse looks much more like the LCL ellipse even in case of very thick pens.
The patch also removes variables not needed any more, as well as an unneeded with bounds do
.