7

I'm trying to send a screenshot of a window over tcp to a server. Getting the screenshot is no problem (using GDIplus). The networking is also easy for me. The problem is trying to convert the gdi+ Bitmap to a png (in memory) to get the data out of it and send it to the server. Can anyone help me please?

surreal
  • 113
  • 1
  • 5
  • Not an endorsement, but FreeImage might help... http://freeimage.sourceforge.net – Mark Setchell Jul 17 '18 at 14:52
  • Similar question here https://stackoverflow.com/questions/39551863/creating-gdi-bitmaps-in-memory-and-then-saving-as-png – seccpur Jul 17 '18 at 15:14
  • Not really similar, since I want to send it as a string over tcp, whereas that guy wanted to save it to a file – surreal Jul 17 '18 at 15:39
  • OpenCV can convert a memory-based image to PNG with `imencode()` but that is an enormous dependency to import. Instead, you could use `NetPBM` tools and shell out via `popen()` to do `bmptopnm()` followed by `pnmtopng()` http://netpbm.sourceforge.net/doc/pnmtopng.html and the NetPBM stuff is miles smaller than OpenCV. – Mark Setchell Jul 17 '18 at 16:11
  • This may help... https://stackoverflow.com/a/22580958/2836621 – Mark Setchell Jul 17 '18 at 16:13

1 Answers1

9

Gdiplus can save to file, or save to memory using IStream. See Gdiplus::Image::Save method

//get gdi+ bitmap
Gdiplus::Bitmap bitmap(hbitmap, nullptr);

//write to IStream
IStream* istream = nullptr;
HRESULT hr = CreateStreamOnHGlobal(NULL, TRUE, &istream);
CLSID clsid_png;
CLSIDFromString(L"{557cf406-1a04-11d3-9a73-0000f81ef32e}", &clsid_png);
bitmap.Save(istream, &clsid_png);

The memory size is small enough that you can copy from IStream to a single buffer (see "Minimum example" for more detail)

//get memory handle associated with istream
HGLOBAL hg = NULL;
GetHGlobalFromStream(istream, &hg);

//copy IStream to buffer
int bufsize = GlobalSize(hg);
char *buffer = new char[bufsize];

//lock & unlock memory
LPVOID ptr = GlobalLock(hg);
memcpy(buffer, ptr, bufsize);
GlobalUnlock(hg);

//release will automatically free the memory allocated in CreateStreamOnHGlobal 
istream->Release();

PNG is now available in buffer, its size is bufsize. You can work directly with the binary data, or convert to Base64 to send over the network.

Minimum example:

#include <iostream>
#include <fstream>
#include <vector>
#include <Windows.h>
#include <gdiplus.h>

bool save_png_memory(HBITMAP hbitmap, std::vector<BYTE>& data)
{
    Gdiplus::Bitmap bmp(hbitmap, nullptr);

    //write to IStream
    IStream* istream = nullptr;
    if (CreateStreamOnHGlobal(NULL, TRUE, &istream) != 0)
        return false;

    CLSID clsid_png;
    if (CLSIDFromString(L"{557cf406-1a04-11d3-9a73-0000f81ef32e}", &clsid_png)!=0)
        return false;
    Gdiplus::Status status = bmp.Save(istream, &clsid_png);
    if (status != Gdiplus::Status::Ok)
        return false;

    //get memory handle associated with istream
    HGLOBAL hg = NULL;
    if (GetHGlobalFromStream(istream, &hg) != S_OK)
        return 0;

    //copy IStream to buffer
    int bufsize = GlobalSize(hg);
    data.resize(bufsize);

    //lock & unlock memory
    LPVOID pimage = GlobalLock(hg);
    if (!pimage)
        return false;
    memcpy(&data[0], pimage, bufsize);
    GlobalUnlock(hg);

    istream->Release();
    return true;
}

int main()
{
    CoInitialize(NULL);

    ULONG_PTR token;
    Gdiplus::GdiplusStartupInput tmp;
    Gdiplus::GdiplusStartup(&token, &tmp, NULL);

    //take screenshot
    RECT rc;
    GetClientRect(GetDesktopWindow(), &rc);
    auto hdc = GetDC(0);
    auto memdc = CreateCompatibleDC(hdc);
    auto hbitmap = CreateCompatibleBitmap(hdc, rc.right, rc.bottom);
    auto oldbmp = SelectObject(memdc, hbitmap);
    BitBlt(memdc, 0, 0, rc.right, rc.bottom, hdc, 0, 0, SRCCOPY);
    SelectObject(memdc, oldbmp);
    DeleteDC(memdc);
    ReleaseDC(0, hdc);

    //save as png
    std::vector<BYTE> data;
    if(save_png_memory(hbitmap, data))
    {
        //write from memory to file for testing:
        std::ofstream fout("test.png", std::ios::binary);
        fout.write((char*)data.data(), data.size());
    }
    DeleteObject(hbitmap);

    Gdiplus::GdiplusShutdown(token);
    CoUninitialize();

    return 0;
}
Barmak Shemirani
  • 30,904
  • 6
  • 40
  • 77
  • `Bitmap map(currentImg, NULL); IStream* stream = nullptr; HRESULT hr = CreateStreamOnHGlobal(NULL, TRUE, &stream); if (hr != S_OK) { Utils::log(Utils::Warning, "Failed to create stream!"); continue; } if (map.Save(stream, &clsid_png) != Ok) { Utils::log(Utils::Warning, "Failed to save to stream!"); continue; }` Problem: It outputs "Failed to save to stream!", which makes the error location obvious, but why? – surreal Jul 17 '18 at 19:21
  • And how do i format code correctly? With line breaks – surreal Jul 17 '18 at 19:24
  • The error says InvalidParameter (GDI Status) btw, I'm currently on the search but help is welcome! – surreal Jul 17 '18 at 19:34
  • Invalid parameter usually means `HBITMAP` handle was invalid. What compiler are you using? See the edit, try the MCVE. – Barmak Shemirani Jul 17 '18 at 19:43
  • Still doesnt work, tried your solution but it gives me the FileNotFound error after the Gdiplus::Bitmap constructor. I'm using visual studio, c++17. Heres' my code: http://prntscr.com/k7wqu8 – surreal Jul 18 '18 at 10:43
  • Nevermind, I had changed the string in CLSIDFromString, which I now changed back and now it works. Thank you very much everyone! – surreal Jul 18 '18 at 11:28
  • what is `hg` in your 2nd snippet ? – Gray Programmerz Jan 31 '22 at 10:32
  • 1
    @GrayProgrammerz It's `HGLOBAL` value. See "Minimum example" for working code. – Barmak Shemirani Jan 31 '22 at 12:55