Menu

Loading jpeg from non-ui thread randomly gives a black image

2021-11-25
2021-11-26
  • Ruchira Hasaranga

    Hi, I just noticed when loading jpeg from a non-ui thread (task) causes it to decoded to black image randomly.
    Following code demonstrate the issue. This issue mostly occur when I move the mouse on the main window.

    procedure TForm1.DecodeTaskHandler;
    var
      myImage: TImage32;
    begin
      while True do begin
        myImage := TImage32.Create();
    
        if myImage.LoadFromFile('D:\small_white_image.jpg') then begin
          if myImage.Pixel[0,0] = Color32(clBlack) then MessageBox(0, 'Image decode failed','Error', 0);
        end;
    
        myImage.Free;
        sleep(10);
       end;
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      TTask.Create(DecodeTaskHandler).Start;
    end;
    
     
  • Ruchira Hasaranga

    Just found out that the bug occur within the Img32.Fmt.JPG.pas -> LoadFromStream method.
    In the following line, Canvas.Handle occasionally becomes zero when called from background thread.

    img32.CopyFromDC(Canvas.Handle, Rect(0,0, Width, Height));
    

    Then I realized the issue comes from the TJpegImage class. TJpegImage.Canvas.Handle occasionally becomes zero when called from a background thread.

    After searching more on the subject, I found following two links:
    https://en.delphipraxis.net/topic/1292-why-is-this-code-not-thread-safe-delphi-7/?do=findComment&comment=10765
    http://www.delphigroups.info/1/6/28001.html

    According to those links, we need to wrap the CopyFromDC within the Canvas.Lock and Canvas.Unlock methods.

    Here is my fix for TImageFormat_JPG.LoadFromStream method:

    function TImageFormat_JPG.LoadFromStream(stream: TStream; img32: TImage32): Boolean;
    var
      jpeg: TJpegImage;
    begin
      result := false;
      jpeg := TJpegImage.Create;
      try
        jpeg.LoadFromStream(stream);
        if jpeg.Empty then Exit;
    
        // lock only if background thread
        if GetCurrentThreadId <> System.MainThreadID then jpeg.Canvas.Lock;
    
        with TJpegImageHack(jpeg).Bitmap do begin
         img32.CopyFromDC(Canvas.Handle, Rect(0,0, Width, Height));
        end;
    
        if GetCurrentThreadId <> System.MainThreadID then jpeg.Canvas.Unlock;
    
        result := true;
      finally
        jpeg.Free;
      end;
    end;
    
     
  • Angus Johnson

    Angus Johnson - 2021-11-26

    Thank you for the very helpful feedback. I'll make the changes you suggested. Cheers.