Graphics.TBitmap successfully decoded from a JPEG

2019-07-09 19:52发布

问题:

I am decoding JPEG frames to Graphics.TBitmap objects using the FastJpeg library (jpegdec.pas). The decode works fine and I print the bitmap to file for visual inspection using the TBitmap.SaveToFile() method and it looks great. I then call GetObject() using the TBitmap handle to get a TDibSection object. The TDibSection object returned does show the correct values for the top level fields (bmWidth, bmHeight, etc.) although bmBit is NIL and I find that surprising seeing as the SaveToFile() call did write the image out to disk correctly. The problem I'm having is that the TBitmapHeaderInfo field (dsBmih) is all zeroes. Also, the dsBitFields, dshSection, and dsOffset fields are all zeroes too if that matters. It's as if it filled in the primary fields and everything after that was left out. Here's a dump of the TDibSection object returned:

dsBm: (0, 320, 240, 1280, 1, 32, nil)
dsBmih: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
dsBitfields: (0, 0, 0)
dshSection: 0
dsOffset: 0

The code below shows bascially what I am doing. Why am I getting back a blank TBitmapHeaderInfo field? This is causing problems with my calls to the AVI dll so I need to fix this.

Here's the code snippet

var
    theBitmap: Graphics.TBitmap;
    aryBytes: TDynamicByteArray;
    dibs: TDibSection;
begin
    aryBytes := nil;

    // The following function loads JPEG frame #0 from a collection of JPEG frames.
    aryBytes := LoadJpegFrame(0);

    // Decode the first JPEG frame so we can pass it to the compressor
    //  selector call.
    theBitmap := JpegDecode(@aryBytes[0], Length(aryBytes));

    if GetObject(theBitmap.Handle, sizeof(dibs), @dibs) = 0 then
        raise Exception.Create('Get Object failed getting the TDibSection information for the bitmap.');

    // ... The TBitmapHeaderInfo field in dibs is empty as described in the post.
end;

UPDATE: In response to the comment by TLama I have updated the code as you can see below. It now works. I have some questions:

1) Can the code be streamlined? It is obviously a lot more complicated than the original code above and perhaps I am performing too many steps.

2) Am I freeing all the memory and releasing all the GDI handles & objects that I need to? I don't want any memory leaks.

Here is the updated code:

var
    hr: HRESULT;
    bmi: TBitmapInfo;
    pImg: PJpegDecode;
    jpegDecodeErr: TJpegDecodeError;
    hbm: HBITMAP;
    pBits: Pointer;
begin
    hr := 0; pImg := nil; hbm := 0; pBits := nil;

    try
        jpegDecodeErr := JpegDecode(@aryBytes[0], Length(aryBytes), pImg);

        if jpegDecodeErr <> JPEG_SUCCESS then
            raise Exception.Create('(TfrmMain_moviemaker_.cmdTestClick) The bitmap failed to decode with error code: ' + IntToStr(Ord(jpegDecodeErr)));

        if not Assigned(pImg) then
            raise Exception.Create('(TfrmMain_moviemaker_.cmdTestClick) The bitmap decoded from the first JPEG frame in the video file is unassigned: ' + fullVideoInFilename);

        pImg^.ToBMI(bmi);

        theBitmap := pImg.ToBitmap;

        // Now create a DIB section.
        hbm := CreateDIBSection(theBitmap.Handle, bmi, DIB_RGB_COLORS, pBits, 0, 0);

        if hbm = ERROR_INVALID_PARAMETER then
            raise Exception.Create('(TfrmMain_moviemaker_.cmdTestClick) One of the parameters passed to CreateDIBSection is invalid.');

        if hbm = 0 then
            raise Exception.Create('(TfrmMain_moviemaker_.cmdTestClick) The call to CreateDIBSection failed.');

        // Select the compressor.  This call USED to fail before TLama's
        //  suggestion.  Now it works.
        hr := aviMaker.compression(hbm, nil, true, Self.Handle);

        if hr <> S_OK then
            raise Exception.Create('(TfrmMain_moviemaker_.cmdTestClick) Error during compressor selector call: ' + FormatAviMessage(hr));
    finally
        if Assigned(pImg) then
        begin
            pImg^.Free;
            pImg := nil;
        end;

        if Assigned(theBitmap) then
            FreeAndNil(theBitmap);

        if hbm > 0 then
            DeleteObject(hbm);
    end; // try (2)
end;

回答1:

Since you need only a handle to the DIB section of the first frame decoded from the JPEG file, I think it should be enough to use the CreateDIBSection and copy to the block of memory allocated by the function call the content pointed by the TJpegDecode.pRGB, which should be the DIB section values already.

procedure ProbeGetObject(ACanvas: TCanvas; AGDIObject: HGDIOBJ);
var
  Width, Height: Integer;
  DIBSection: TDIBSection;
  BitmapInfo: TBitmapInfo;
begin
  if GetObject(AGDIObject, SizeOf(DIBSection), @DIBSection) <> 0 then
  begin
    FillChar(BitmapInfo, SizeOf(BitmapInfo), 0);
    BitmapInfo.bmiHeader := DIBSection.dsBmih;
    // if you comment the following line, the image will be rendered flipped
    BitmapInfo.bmiHeader.biHeight := - BitmapInfo.bmiHeader.biHeight;
    Width := DIBSection.dsBm.bmWidth;
    Height := Abs(DIBSection.dsBm.bmHeight);
    StretchDIBits(ACanvas.Handle, 0, 0, Width, Height, 0, 0, Width, Height,
      DIBSection.dsBm.bmBits, BitmapInfo, DIB_RGB_COLORS, SRCCOPY);
  end;
end;

procedure InitializeCompressor(Buffer: Pointer; BufferLen: Integer);
var
  ScreenDC: HDC;
  DIBHandle: HBITMAP;
  DIBValues: Pointer;
  BufferSize: DWORD;
  BufferHandle: THandle;
  BufferPointer: Pointer;
  JPEGImage: PJpegDecode;
  BitmapInfo: TBitmapInfo;
begin
  if JpegDecode(Buffer, BufferLen, JPEGImage) = JPEG_SUCCESS then
  try
    JPEGImage^.ToBMI(BitmapInfo);
    BufferSize := Abs(BitmapInfo.bmiHeader.biWidth *
      BitmapInfo.bmiHeader.biHeight * 4);
    BufferHandle := CreateFileMapping(INVALID_HANDLE_VALUE, nil,
      PAGE_READWRITE, 0, BufferSize, nil);
    if BufferHandle <> 0 then
    try
      BufferPointer := MapViewOfFile(BufferHandle, FILE_MAP_WRITE, 0, 0, 0);
      if Assigned(BufferPointer) then
      begin
        CopyMemory(BufferPointer, JPEGImage^.pRGB, BufferSize);
        ScreenDC := GetDC(0);
        if ScreenDC <> 0 then
        try
          DIBHandle := CreateDIBSection(ScreenDC, BitmapInfo, DIB_RGB_COLORS,
            DIBValues, BufferHandle, 0);
          if (DIBHandle <> 0) and Assigned(DIBValues) then
          try
            ProbeGetObject(Form1.Canvas, DIBHandle);
            // here try to initialize the compressor, the DIB section should
            // contain values obtained from the JPEG decoder; in the DIBHandle
            // variable is the handle to the DIB section
          finally
            DeleteObject(DIBHandle);
          end;
        finally
          ReleaseDC(0, ScreenDC);
        end;
      end;
    finally
      CloseHandle(BufferHandle);
    end;
  finally
    JPEGImage^.Free;
  end;
end;