Search code examples
delphiimageimage-processingflood-fill

Is it possible to use "Fill with color" function on Delphi's Image component?


I have a TImage component on the form. I need to implement the following functionality:

(If mouse pointer is over point with red color, then apply "Fill with color green" to that point)

Here by "Fill with color" I mean Paint's function "Fill with color". Is there anything similar in TImage? Or should I implement this function myself?

Thank you

P.S. I use Delphi 7


Solution

  • I guess you are talking about "flood fill". Some time ago, I wrote my own implementation of this based on the Wikipedia article. I represent the bitmap as a two-dimensional array of TRGBQuad pixels.

    function PMFloodFill(Pixmap: TASPixmap; const X0: integer; const Y0: integer; const Color: TColor): TASPixmap;
    var
      w, h: integer;
      MatchColor, QColor: TRGBQuad;
      Queue: packed {SIC!} array of TPoint;
      cp: TPoint;
    
      procedure push(Point: TPoint);
      begin
        SetLength(Queue, length(Queue) + 1);
        Queue[High(Queue)] := Point;
      end;
    
      function pop: TPoint;
      var
        lm1: integer;
      begin
        assert(length(Queue) > 0);
        result := Queue[0];
        lm1 := length(Queue) - 1;
        if lm1 > 0 then
          MoveMemory(@(Queue[0]), @(Queue[1]), lm1 * sizeof(TPoint));
        SetLength(Queue, lm1);
      end;
    
    begin
      PMSize(Pixmap, h, w);
      result := Pixmap;
      if not (IsIntInInterval(X0, 0, w-1) and IsIntInInterval(Y0, 0, h-1)) then
        Exit;
      // Find color to match
      MatchColor := Pixmap[Y0, X0];
      QColor := PascalColorToRGBQuad(Color);
      SetLength(Queue, 0);
      push(point(X0, Y0));
      while length(Queue) > 0 do
      begin
        if RGBQuadEqual(result[Queue[0].Y, Queue[0].X], MatchColor) then
          result[Queue[0].Y, Queue[0].X] := QColor;
    
        cp := pop;
    
        if cp.X > 0 then
          if RGBQuadEqual(result[cp.Y, cp.X - 1], MatchColor) then
          begin
            result[cp.Y, cp.X - 1] := QColor;
            push(point(cp.X - 1, cp.Y));
          end;
    
        if cp.X < w-1 then
          if RGBQuadEqual(result[cp.Y, cp.X + 1], MatchColor) then
          begin
            result[cp.Y, cp.X + 1] := QColor;
            push(point(cp.X + 1, cp.Y));
          end;
    
        if cp.Y > 0 then
          if RGBQuadEqual(result[cp.Y - 1, cp.X], MatchColor) then
          begin
            result[cp.Y - 1, cp.X] := QColor;
            push(point(cp.X, cp.Y - 1));
          end;
    
        if cp.Y < h-1 then
          if RGBQuadEqual(result[cp.Y + 1, cp.X], MatchColor) then
          begin
            result[cp.Y + 1, cp.X] := QColor;
            push(point(cp.X, cp.Y + 1));
          end;
      end;
    end;
    

    The complete code is

    unit Unit4;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, ComCtrls, ToolWin;
    
    type
      TForm4 = class(TForm)
        ToolBar1: TToolBar;
        ToolButton1: TToolButton;
        ToolButton2: TToolButton;
        procedure ToolButton1Click(Sender: TObject);
        procedure FormCreate(Sender: TObject);
        procedure FormPaint(Sender: TObject);
        procedure ToolButton2Click(Sender: TObject);
        procedure FormMouseDown(Sender: TObject; Button: TMouseButton;
          Shift: TShiftState; X, Y: Integer);
      private
        procedure UpdateBitmap(Sender: TObject);
        { Private declarations }
      public
        { Public declarations }
      end;
    
    var
      Form4: TForm4;
      bm: TBitmap;
      CurrentColor: TColor = clRed;
    
    implementation
    
    {$R *.dfm}
    
    type
      TASPixmap = array of packed array of TRGBQuad;
    
      TRGB32Array = packed array[0..MaxInt div SizeOf(TRGBQuad)-1] of TRGBQuad;
      PRGB32Array = ^TRGB32Array;
    
      TScanline = TRGB32Array;
      PScanline = ^TScanline;
    
    function IsIntInInterval(x, xmin, xmax: integer): boolean; {inline;}
    begin
      IsIntInInterval := (xmin <= x) and (x <= xmax);
    end;
    
    function PascalColorToRGBQuad(const Color: TColor): TRGBQuad;
    begin
      with Result do
      begin
        rgbBlue := GetBValue(Color);
        rgbGreen := GetGValue(Color);
        rgbRed := GetRValue(Color);
        rgbReserved := 0;
      end;
    end;
    
    function RGBQuadEqual(const Color1: TRGBQuad; const Color2: TRGBQuad): boolean;
    begin
      RGBQuadEqual := (Color1.rgbBlue = Color2.rgbBlue) and
                      (Color1.rgbGreen = Color2.rgbGreen) and
                      (Color1.rgbRed = Color2.rgbRed);
    end;
    
    function PMFloodFill(Pixmap: TASPixmap; const X0: integer; const Y0: integer; const Color: TColor): TASPixmap;
    var
      w, h: integer;
      MatchColor, QColor: TRGBQuad;
      Queue: packed {SIC!} array of TPoint;
      cp: TPoint;
    
      procedure push(Point: TPoint);
      begin
        SetLength(Queue, length(Queue) + 1);
        Queue[High(Queue)] := Point;
      end;
    
      function pop: TPoint;
      var
        lm1: integer;
      begin
        assert(length(Queue) > 0);
        result := Queue[0];
        lm1 := length(Queue) - 1;
        if lm1 > 0 then
          MoveMemory(@(Queue[0]), @(Queue[1]), lm1 * sizeof(TPoint));
        SetLength(Queue, lm1);
      end;
    
    begin
      h := length(Pixmap);
      if h > 0 then
        w := length(Pixmap[0]);
      result := Pixmap;
      if not (IsIntInInterval(X0, 0, w-1) and IsIntInInterval(Y0, 0, h-1)) then
        Exit;
      // Find color to match
      MatchColor := Pixmap[Y0, X0];
      QColor := PascalColorToRGBQuad(Color);
      SetLength(Queue, 0);
      push(point(X0, Y0));
      while length(Queue) > 0 do
      begin
        if RGBQuadEqual(result[Queue[0].Y, Queue[0].X], MatchColor) then
          result[Queue[0].Y, Queue[0].X] := QColor;
    
        cp := pop;
    
        if cp.X > 0 then
          if RGBQuadEqual(result[cp.Y, cp.X - 1], MatchColor) then
          begin
            result[cp.Y, cp.X - 1] := QColor;
            push(point(cp.X - 1, cp.Y));
          end;
    
        if cp.X < w-1 then
          if RGBQuadEqual(result[cp.Y, cp.X + 1], MatchColor) then
          begin
            result[cp.Y, cp.X + 1] := QColor;
            push(point(cp.X + 1, cp.Y));
          end;
    
        if cp.Y > 0 then
          if RGBQuadEqual(result[cp.Y - 1, cp.X], MatchColor) then
          begin
            result[cp.Y - 1, cp.X] := QColor;
            push(point(cp.X, cp.Y - 1));
          end;
    
        if cp.Y < h-1 then
          if RGBQuadEqual(result[cp.Y + 1, cp.X], MatchColor) then
          begin
            result[cp.Y + 1, cp.X] := QColor;
            push(point(cp.X, cp.Y + 1));
          end;
      end;
    end;
    
    function GDIBitmapToASPixmap(const Bitmap: TBitmap): TASPixmap;
    var
      scanline: PScanline;
      width, height, bytewidth: integer;
      y: Integer;
    begin
    
      Bitmap.PixelFormat := pf32bit;
    
      width := Bitmap.Width;
      height := Bitmap.Height;
      bytewidth := width * 4;
    
      SetLength(Result, height);
      for y := 0 to height - 1 do
      begin
        SetLength(Result[y], width);
        scanline := @(Result[y][0]);
        CopyMemory(scanline, Bitmap.ScanLine[y], bytewidth);
      end;
    
    end;
    
    procedure GDIBitmapAssign(Bitmap: TBitmap; const Pixmap: TASPixmap);
    var
      y: Integer;
      scanline: PScanline;
      bytewidth: integer;
    begin
      Bitmap.PixelFormat := pf32bit;
      Bitmap.SetSize(length(Pixmap[0]), length(Pixmap));
      bytewidth := Bitmap.Width * 4;
    
      for y := 0 to Bitmap.Height - 1 do
      begin
        scanline := @(Pixmap[y][0]);
        CopyMemory(Bitmap.ScanLine[y], scanline, bytewidth);
      end;
    end;
    
    procedure TForm4.FormCreate(Sender: TObject);
    begin
      bm := TBitmap.Create;
    end;
    
    procedure TForm4.FormMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    var
      x0, y0: integer;
      pm: TASPixmap;
    begin
      x0 := X;
      y0 := Y - ToolBar1.Height;
    
      if IsIntInInterval(x0, 0, bm.Width) and IsIntInInterval(y0, 0, bm.Height) then
      begin
        pm := GDIBitmapToASPixmap(bm);
        pm := PMFloodFill(pm, x0, y0, CurrentColor);
        GDIBitmapAssign(bm, pm);
        UpdateBitmap(Self);
      end;
    end;
    
    procedure TForm4.FormPaint(Sender: TObject);
    begin
      Canvas.Draw(0, ToolBar1.Height, bm);
    end;
    
    procedure TForm4.UpdateBitmap(Sender: TObject);
    begin
      Invalidate;
    end;
    
    procedure TForm4.ToolButton1Click(Sender: TObject);
    begin
      with TOpenDialog.Create(self) do
        try
          Filter := 'Windows Bitmaps (*.bmp)|*.bmp';
          Title := 'Open Bitmap';
          Options := [ofPathMustExist, ofFileMustExist];
          if Execute then
          begin
            bm.LoadFromFile(FileName);
            UpdateBitmap(Sender);
          end;
        finally
          Free;
        end;
    end;
    
    procedure TForm4.ToolButton2Click(Sender: TObject);
    begin
      with TColorDialog.Create(self) do
        try
          Color := CurrentColor;
          Options := [cdFullOpen];
          if Execute then
            CurrentColor := Color;
        finally
          Free;
        end;
    end;
    
    end.
    

    Flood Fill Sample Application

    Project files

    For your convenience, you can download the entire project from

    Don't forget the sample bitmap.