0

I have a screenshot in my clipboard.

I have used win32clipboard.GetClipboardData(win32con.CF_DIB) to get a string, and written the string to a .bmp file, but it could not be opened by a picture viewer.

So, without PIL (and other image third part libs), how to write the image in clipboard to local?

martineau
  • 119,623
  • 25
  • 170
  • 301
Kingname
  • 1,302
  • 11
  • 25

1 Answers1

2

The primary problem with your simple approach is that the string written to the file is missing a .bmp file header, which is a BITMAPFILEHEADER structure.

In order to create the file header at least some of the information in the string returned by the GetClipboardData() call must be decoded. For CF_DIB clipboard format, the first part of the data in the string will be a BITMAPINFOHEADER.

This header structure is a very general as there are many different flavors of DIBs with various bits-per-component and kinds-of-compression. Fortunately the one used for screenshots is very simple — uncompressed RGBA pixels.

That fact makes things much easier because otherwise determining the value to put in the bfOffBits field of the BITMAPFILEHEADER would be complicated by the fact that in most other cases there's also a variably-sized color table following the BITMAPINFOHEADER and the start of the pixel array.

Below is example code that handles that case (only):

import ctypes
from ctypes.wintypes import *
import win32clipboard
from win32con import *
import sys

class BITMAPFILEHEADER(ctypes.Structure):
    _pack_ = 1  # structure field byte alignment
    _fields_ = [
        ('bfType', WORD),  # file type ("BM")
        ('bfSize', DWORD),  # file size in bytes
        ('bfReserved1', WORD),  # must be zero
        ('bfReserved2', WORD),  # must be zero
        ('bfOffBits', DWORD),  # byte offset to the pixel array
    ]
SIZEOF_BITMAPFILEHEADER = ctypes.sizeof(BITMAPFILEHEADER)

class BITMAPINFOHEADER(ctypes.Structure):
    _pack_ = 1  # structure field byte alignment
    _fields_ = [
        ('biSize', DWORD),
        ('biWidth', LONG),
        ('biHeight', LONG),
        ('biPLanes', WORD),
        ('biBitCount', WORD),
        ('biCompression', DWORD),
        ('biSizeImage', DWORD),
        ('biXPelsPerMeter', LONG),
        ('biYPelsPerMeter', LONG),
        ('biClrUsed', DWORD),
        ('biClrImportant', DWORD)
    ]
SIZEOF_BITMAPINFOHEADER = ctypes.sizeof(BITMAPINFOHEADER)

win32clipboard.OpenClipboard()
try:
    if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
        data = win32clipboard.GetClipboardData(win32clipboard.CF_DIB)
    else:
        print('clipboard does not contain an image in DIB format')
        sys.exit(1)
finally:
    win32clipboard.CloseClipboard()

bmih = BITMAPINFOHEADER()
ctypes.memmove(ctypes.pointer(bmih), data, SIZEOF_BITMAPINFOHEADER)

if bmih.biCompression != BI_BITFIELDS:  # RGBA?
    print('insupported compression type {}'.format(bmih.biCompression))
    sys.exit(1)

bmfh = BITMAPFILEHEADER()
ctypes.memset(ctypes.pointer(bmfh), 0, SIZEOF_BITMAPFILEHEADER)  # zero structure
bmfh.bfType = ord('B') | (ord('M') << 8)
bmfh.bfSize = SIZEOF_BITMAPFILEHEADER + len(data)  # file size
SIZEOF_COLORTABLE = 0
bmfh.bfOffBits = SIZEOF_BITMAPFILEHEADER + SIZEOF_BITMAPINFOHEADER + SIZEOF_COLORTABLE

bmp_filename = 'clipboard.bmp'
with open(bmp_filename, 'wb') as bmp_file:
    bmp_file.write(bmfh)
    bmp_file.write(data)

print('file "{}" created from clipboard image'.format(bmp_filename))
martineau
  • 119,623
  • 25
  • 170
  • 301
  • you are a master! thanks and this is what I want. BTW, I want to translate this answer to Chinese and post to my blog, so I want to get your permission, please allow me to do that. – Kingname Mar 09 '16 at 13:48
  • You're welcome. Yes, feel free to use it in your blog — be sure to use the updated version I just posted where I tried to improve the explanation (no code changes). – martineau Mar 09 '16 at 14:27
  • Please post a link to the blog entry here as a comment. I'd like to see it even if I can't read the portion of it in Chinese. Thanks. – martineau Mar 09 '16 at 14:34
  • Dear Sir, I have post the blog here: [http://kingname.info/2016/03/11/shi-yong-pythonbao-cun-jie-tu/](http://kingname.info/2016/03/11/shi-yong-pythonbao-cun-jie-tu/) – Kingname Mar 11 '16 at 13:43
  • and more question I want to ask, for the code, `bmfh.bfType = ord('B') | (ord('M') << 8)` why you use B and M to get 100110101000010? and why 100110101000010 make sense? Thanks anyway. – Kingname Mar 11 '16 at 13:44
  • I'm looking at an [English translation](https://translate.google.com/translate?sl=auto&tl=en&js=y&prev=_t&hl=en&ie=UTF-8&u=http%3A%2F%2Fkingname.info%2F2016%2F03%2F11%2Fshi-yong-pythonbao-cun-jie-tu%2F&edit-text=) of your blog post now — thanks. To answer your question: The `bfType` field is declared as a `WORD`, which is a numeric type (`c_ushort`). That part of a `.bmp` file header must contain the ASCII characters "BM" as a signature, which is a string, so the code is simply calculating the integer value equivalent to that because `bmfh.bfType = "BM"` would result in a `TypeError`. – martineau Mar 11 '16 at 14:32
  • I just finished reading all of the translated blog post. Frankly I think you misunderstood what what the code in my answer is doing (as well as part of my own explanation of what it needs to do). If you would like specific feedback, email me at the address in my SO profile — we shouldn't use comments to do this. – martineau Mar 11 '16 at 15:31
  • Excuse me sir, I have sent you the mail weeks ago by a foxmail.com mail address, but I have not received your mail until now. Have you gotten my mail? or Did I miss your mail? – Kingname Mar 20 '16 at 14:46
  • I haven't received your message. However, I just checked my spam folder and it was there...so will be reading and responding if necessary soon. – martineau Mar 21 '16 at 12:52