Search code examples
c++screenshotgdi+gdi

GDI+ screenshot doesn't work with odd resolutions


Hi I am trying to create a Screen Capturing class in c++ (since python screenshots are slow as heck). I created this class but I can't create screenshots with odd resolutions like 101x101.

I get a bunch of heap corruption and access violation errors in VS and I have no clue how to debug this. Appreciate any hints/tips.

Here is the class:

#include <windows.h>
#include <gdiplus.h>
#include <iostream>
#include <opencv2/opencv.hpp>

#pragma comment (lib,"Gdiplus.lib")

class ScreenCapturer {
private:
    // GDI+ token and startup input
    ULONG_PTR gdiplusToken;
    Gdiplus::GdiplusStartupInput gdiplusStartupInput;

    // Device contexts
    HDC hScreenDC;
    HDC hMemoryDC;

    // Screen dimensions
    int lastWidth;
    int lastHeight;

    // Bitmap
    HBITMAP hBitmap;
    HBITMAP hOldBitmap; // Store the old bitmap here to use for recovery and cleanup

public:
    ScreenCapturer(){
        Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

        hScreenDC = CreateDC(L"DISPLAY", NULL, NULL, NULL);
        if (hScreenDC == NULL) {
            throw std::runtime_error("Failed to create screen device context");
        }
 
        hMemoryDC = CreateCompatibleDC(hScreenDC);
        if (hMemoryDC == NULL) {
            DeleteDC(hScreenDC);
            throw std::runtime_error("Failed to create memory device context.");
        }

        lastWidth = GetDeviceCaps(hScreenDC, HORZRES);
        lastHeight = GetDeviceCaps(hScreenDC, VERTRES);

        hBitmap = CreateCompatibleBitmap(hScreenDC, lastWidth, lastHeight);
        if (!hBitmap) {
            DeleteDC(hMemoryDC);
            DeleteDC(hScreenDC);
            throw std::runtime_error("Failed to create compatible bitmap");
        }

        hOldBitmap = (HBITMAP)SelectObject(hMemoryDC, hBitmap);
    }

    ~ScreenCapturer() {
        SelectObject(hMemoryDC, hOldBitmap);
        DeleteDC(hMemoryDC);
        DeleteDC(hScreenDC);
        DeleteObject(hBitmap);
        Gdiplus::GdiplusShutdown(gdiplusToken);
    }

    cv::Mat captureScreen(int x, int y, int width, int height) {
        if (lastWidth != width || lastHeight != height) {
            SelectObject(hMemoryDC, hOldBitmap);
            DeleteObject(hBitmap);
            hBitmap = CreateCompatibleBitmap(hScreenDC, width, height);
            /*hOldBitmap = (HBITMAP)*/SelectObject(hMemoryDC, hBitmap); // Do we need to re-store the old bitmap?
            lastWidth = width;
            lastHeight = height;
        }

        if (BitBlt(hMemoryDC, 0, 0, width, height, hScreenDC, x, y, SRCCOPY) == NULL) {
            std::cout << "ERROR " << std::endl;
        }

        BITMAPINFOHEADER bi;
        bi.biSize = sizeof(BITMAPINFOHEADER);
        bi.biWidth = width;
        bi.biHeight = -height; // Negative for top-left origin
        bi.biPlanes = 1;
        bi.biBitCount = 24;
        bi.biCompression = BI_RGB;
        bi.biSizeImage = 0;
        bi.biXPelsPerMeter = 0;
        bi.biYPelsPerMeter = 0;
        bi.biClrUsed = 0;
        bi.biClrImportant = 0;

        // Allocate a cv::Mat for the area
        cv::Mat areaImage(height, width, CV_8UC3);

        // Copy the bits to the cv::Mat
        if (GetDIBits(hMemoryDC, hBitmap, 0, (UINT)height, areaImage.data, (BITMAPINFO*)&bi, DIB_RGB_COLORS) == NULL) {
            std::cout << "ERROR " << std::endl;
        }

        return areaImage;
    }
};

I am testing this class like this:

#include "ScreenCapturer.cpp"
int main() {
    ScreenCapturer capturer;
    auto screenshot1 = capturer.captureScreen(0, 0, 1920, 1080); // Works
    cv::imwrite("test1.png", screenshot1);
    auto screenshot2 = capturer.captureScreen(0, 0, 100, 100); // Works
    cv::imwrite("test2.png", screenshot2);
    auto screenshot3 = capturer.captureScreen(0, 0, 101, 101); // Doesn't work
    cv::imwrite("test3.png", screenshot3);
    std::cout << "Done\n";
}

I tried to simply get more info from VS. When it fails in debug mode it first comes up with a Breakpoint Instruction Executed with this message:

A breakpoint instruction (__debugbreak() statement or a similar call) was executed in Everylay.exe.

And then when I continue it follows up with messages like this:

Unhandled exception at 0x00007FF9A7A4F349 (ntdll.dll) in Everylay.exe: 0xC0000374: A heap has been corrupted (parameters: 0x00007FF9A7AB97F0).

And then this:

Exception thrown at 0x00007FF9A797D990 (ntdll.dll) in Everylay.exe: 0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF.

I am really clueless how to debug this. Hope someone can help out.


Solution

  • Buffer pointed to by lpvBits argument of GetDIBits must have size sufficient to hold height * scanline_width bytes. Where scanline_width is img_width * bytes_per_pixel with 4 bytes alignment. With 101 image width and 3 bytes per pixel buffer provided by cv::mat will have insufficient size since it does not align scanlines properly. So you are getting a buffer overflow.

    I should also mention that you using only plain GDI functions so GdiplusStartup and GdiplusShutdown are not required.