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

alphablend-bug

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:

alphablend-fixed

The second screenshot is for black opaque text demonstrating that text in this color is still drawn correctly after the change.

alphablend

Patch

text-background.diff

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.

Assignee Loading
Time tracking Loading