FCL-Image: Artefacts of drawing text over alpha-blended areas
Summary
Drawing text in a TFPMemoryImage (as proposed by https://wiki.freepascal.org/fcl-image#Drawing_text) over semitransparent, alpha-blended areas (as proposed by https://wiki.freepascal.org/fcl-image#Drawing_modes) shows artefacts in the character bounding boxes where are clearly visible as de-colored rectangles.
System Information
- Operating system: Windows 11
- Processor architecture: x86-64
- Compiler version: 3.2.2, but also trunk (0d0b5df6)
- Device: Computer
Steps to reproduce
The attached project, in AlphaBlend mode, draws an opaque yellow rectangle plus two semi-transparent circles. At the end, some dummy text is painted over them. No matter which color and alpha is selected for the text color, in each case there are shaded, de-colored rectangles over the semitransparent shapes which enclose each text character. In the attached screenshot "alphablend_bug.png", the effect is particularly visible for the "T" and the "s". On the opaque rectangle, the effect does not occur.
Example Project
program alphablend;
uses
FPImage, FPImgCanv, FPCanvas, FTFont, FPWritePNG;
var
img: TFPMemoryImage;
canvas: TFPImageCanvas;
writer: TFPWriterPNG;
font: TFreeTypeFont;
begin
// initialize free type font manager
FTFont.InitEngine;
{$IFNDEF MSWINDOWS}
FontMgr.SearchPath:='/usr/share/fonts/truetype/ttf-dejavu/'; // not needed on Windows
{$ENDIF}
font:=TFreeTypeFont.Create;
img := TFPMemoryImage.Create(200, 200);
canvas := TFPImageCanvas.Create(img);
try
canvas.DrawingMode := dmAlphaBlend; // This activates the alpha-blend mode
// Background
canvas.Pen.Style := psClear;
canvas.Brush.FPColor := colTransparent;
canvas.Clear;
// Yellow opaque rectangle
canvas.Brush.FPColor := FPColor($FFFF, $FFFF, 0);
canvas.Rectangle(10, 10, 190, 100);
// Overlapping semi-transparent red circle
canvas.Brush.FPColor := FPColor($FFFF, 0, 0, $4000);
canvas.Ellipse(60, 60, 140, 140);
// Overlapping semi-transparent blue circle
canvas.Brush.FPColor := FPColor(0, 0, $FFFF, $8000);
canvas.Ellipse(0, 90, 100, 190);
// Paint text
canvas.Font := font;
canvas.Font.Name := 'DejaVuSans';
canvas.Font.Size := 64;
// Select one of the following lines
canvas.Font.FPColor := colBlack; // opaque black text
// canvas.Font.FPColor($0FFF, $0FFF, $0FFF, $6000); // semitransparent gray text
canvas.TextOut(5, 140, 'Test');
// Write to file
writer := TFPWriterPNG.Create;
try
writer.UseAlpha := true;
img.SaveToFile('alphablend.png', writer);
finally
writer.Free;
end;
finally
canvas.Free;
img.Free;
font.Free;
end;
end.
What is the current bug behavior?
Grayish background of each character on the semitransparent shapes.
What is the expected (correct) behavior?
The background of the character cells should not be discernible
Relevant logs and/or screenshots
Possible fixes
The text, in this sample project, is drawn by unit FTFont. The FreeType rendering engine here store each character in a grayscale bitmap where each pixel is initialized with 0 (black!). In TFreeTypeFont.DrawChar the rectangle of this bitmap is blended over the background for each character:
procedure TFreeTypeFont.DrawChar (x,y:integer; data:PByteArray; pitch, width, height:integer);
procedure Combine (canv:TFPCustomCanvas; x,y:integer; const c : TFPColor; t:longword);
var
pixelcolor: TFPColor;
begin
case canv.DrawingMode of
dmOpaque:
begin
pixelcolor := FpImage.FPColor(c.red, c.green,c.blue, (t+1) shl 8 - 1); // opaque: ignore c.Alpha
canv.colors[x,y] := AlphaBlend(canv.colors[x,y], pixelcolor);
end;
else
pixelcolor := FpImage.FPColor(c.red, c.green,c.blue, ((t+1) shl 8 - 1) * c.Alpha div $ffff); // apply c.Alpha
canv.DrawPixel(x,y,pixelcolor);
end;
end;
var b,rx,ry : integer;
begin
b := 0;
for ry := 0 to height-1 do
begin
for rx := 0 to width-1 do
combine (canvas, x+rx, y+ry, FPColor, data^[b+rx]);
inc (b, pitch);
end;
end;
Since this way also the non-used pixels of each character cell are painted their black color (default value 0) is blended with the background, the image loses its color this way when the image here is not opaque.
As a fix I propose to draw only used character pixels; in above code this can be detected by the value of t: when it is 0 we have a non-used pixel:
procedure Combine (canv:TFPCustomCanvas; x,y:integer; const c : TFPColor; t:longword);
var
pixelcolor: TFPColor;
begin
if t = 0 then exit; // <--- ADDED
case canv.DrawingMode of
dmOpaque:
begin
pixelcolor := FpImage.FPColor(c.red, c.green,c.blue, (t+1) shl 8 - 1); // opaque: ignore c.Alpha
canv.colors[x,y] := AlphaBlend(canv.colors[x,y], pixelcolor);
end;
else
pixelcolor := FpImage.FPColor(c.red, c.green,c.blue, ((t+1) shl 8 - 1) * c.Alpha div $ffff); // apply c.Alpha
canv.DrawPixel(x,y,pixelcolor);
end;
end;
After this modification (adding the line "if t = 0 then exit"), the screenshot of the demo project looks correct, the character boxes are no longer visible:
The second screenshot is for black opaque text demonstrating that text in this color is still drawn correctly after the change.
Patch
Note that there is another drawing method, TFreeTypeFont.DrawCharW. I don't know whether it has the same issue, my patch does not touch it.


