Capture screenshot of program using indexed color table

1.2k views Asked by At

There are numerous tutorial on the web on how to capture and save a screenshot using C#. For example, I used this website to obtain my solution:

        using (var screenshot = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format32bppArgb))
        using (var g = Graphics.FromImage(screenshot))
        {
            g.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, Screen.PrimaryScreen.Bounds.Size);
            screenshot.Save("screenshot.png", ImageFormat.Png);
        }

It works fine in most programs, but the program I want to capture uses an 8 bits indexed color table. Screenshots of that program taken with this code are strange. My question is if someone can point me in the right direction for capturing screenshots of programs with indexed color tables in C#?

To help you help me, I will describe my findings and attempted solutions below.

The screenshots captured with this code of a full screen program using an 8 bits indexed color table is mostly black(for ~88 %) and there are only 17 other colors in it. I don't see a pattern in those 17 colors. In the program itself almost all 256 colors are used. I was hoping to find 256 colors in the black screenshots as well, which could indicate a simple one-on-one relationship, but that is not the case.

I would also like to note that screenshots taken manually are perfect(when I paste them in MS Paint for example). For that reason I tried fetching the image from System.Windows.Forms.Clipboard.GetImage() after taking a screenshot manually, but that returns a COMobject and I wouldn't know what to do with that. Surely that must contain the information of a valid screenshot, since MS Paint knows how to extract it from that COM object. How can I extract that myself, preferably in C#?

But my main question: Can someone point me in the right direction for capturing screenshots of programs with indexed color tables with C#?

3

There are 3 answers

0
JBSnorro On BEST ANSWER

The suggested solutions did not work and I only recently solved this problem. I took a screenshot by sending a PrintScreen key press and release to windows, and acquired the data from the clipboard in the form of a MemoryStream and figured out how the screenshot was decoded in that stream and converted that to a bitmap. Not very appealing, but at least it works...

5
Hans Passant On

That's not how it works. You are capturing the screen, not directly the output of the program. The setting of the video adapter matters, it will be 32bpp for any recent machine. Maybe 16bpp for old ones. Trying to copy such a non-indexed pixel format into a bitmap with a palette isn't supported. The algorithm to create a palette that provides the best color fidelity is computationally quite non-trivial.

Just don't bother, the 32bpp image will be indistinguishable from the program's output. If squeezing the file is really important then store it as a .gif. It isn't going to look great, the GIF encoder uses dithering to compress the color table.

2
Theraot On

If I understand correctly, the problem here is that you are copying the pixel values (the palette indexes) but not the palette itself. I didn't find a way to copy the palette with pure C#, but with P/Invoke and an alternative with DirectX... since both adds a dependency to Windows i've opted for P/Invoke as it's easier and doesn't depend on DirectX.

I have two methods to offer you...

The first Method:

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
    private static extern IntPtr SelectPalette(
        IntPtr hdc,
        IntPtr htPalette,
        bool bForceBackground);

    [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
    private static extern int RealizePalette(IntPtr hdc);

    private void ScreenShot()
    {
        IntPtr htPalette = Graphics.GetHalftonePalette();
        using (var screenshot = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
        {
            using (var graphics = Graphics.FromImage(screenshot))
            {
                IntPtr hdc = graphics.GetHdc();
                SelectPalette(hdc, htPalette, true);
                RealizePalette(hdc);
                graphics.ReleaseHdc(hdc);
                graphics.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, Screen.PrimaryScreen.Bounds.Size);
            }
            screenshot.Save("screenshot.png", System.Drawing.Imaging.ImageFormat.Png);
        }
    }

This method depends on GetHalftonePalette giving you the right palette. I have never tested with palettes (I'm too young)... so I decided to write a second method based in that windows should handle palettes automatically in some conditions. Note: not sure if moving the call to CopyFromScreen to the begin of the using block is better, or ignoring the call to RealizePalette (I'm too young).

The second Method:

    [System.Runtime.InteropServices.DllImport("gdi32.dll")]
    [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
    static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, TernaryRasterOperations dwRop);

    private void ScreenShot()
    {
        int width = Screen.PrimaryScreen.Bounds.Width;
        int height = Screen.PrimaryScreen.Bounds.Height;
        using (var screenshot = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
        {
            using (var fromHwnd = Graphics.FromHwnd(new IntPtr(0)))
            using (var graphics = Graphics.FromImage(screenshot))
            {
                IntPtr hdc_screen = fromHwnd.GetHdc();
                IntPtr hdc_screenshot = graphics.GetHdc();
                BitBlt(hdc_screenshot, 0, 0, width, height, hdc_screen, 0, 0, 0x00CC0020); /*SRCCOPY = 0x00CC0020*/
                graphics.ReleaseHdc(hdc_screenshot);
                fromHwnd.ReleaseHdc(hdc_screen);
            }
            screenshot.Save("screenshot.png", System.Drawing.Imaging.ImageFormat.Png);
        }
    }

Both methods works on normal conditions. Please do your testing, it may be that you will need to do the second method with the addition of duplicating the original palette (that is... when windows doesn't handle them automatically), but I'm not sure on how to do that (I'm too young) and hope this method works.

Lastly, if you are to afford an extra dependency and want it to be different from Windows, maybe GTK# (there is a version available form Windows) is a good option. Please see How to take a screenshot with Mono C#? as it addresses a similar problem, and provides example code for GTK#.

Note: The following code seems to work well here, do you get any exceptions by this code?

System.Drawing.Image image = Clipboard.GetImage();
image.Save("screenshot.png", System.Drawing.Imaging.ImageFormat.Png);