//go:build windows package win32 import ( "fmt" "image" "image/color" "unsafe" ) // GetClipboardImage attempts to read an image from the Windows clipboard. // It supports CF_DIB and CF_DIBV5 (the two formats produced by Snipping // Tool / Win+Shift+S and most other screenshot tools). Returns nil, nil // if the clipboard does not currently hold an image format we can read. func GetClipboardImage() (image.Image, error) { if err := openClipboardRetry(0); err != nil { return nil, fmt.Errorf("win32: OpenClipboard: %w", err) } defer procCloseClipboard.Call() // Try CF_DIBV5 (17) first, then CF_DIB (8). if img, err := readDIBFromClipboard(17); err != nil { return nil, err } else if img != nil { return img, nil } if img, err := readDIBFromClipboard(8); err != nil { return nil, err } else if img != nil { return img, nil } return nil, nil } func readDIBFromClipboard(format uint32) (image.Image, error) { r, _, _ := procGetClipboardData.Call(uintptr(format)) if r == 0 { return nil, nil } src, _, _ := procGlobalLock.Call(r) if src == 0 { return nil, nil } defer procGlobalUnlock.Call(r) size, _, _ := procGlobalSize.Call(r) if size == 0 { return nil, nil } data := unsafe.Slice((*byte)(unsafe.Pointer(src)), size) img := decodeDIB(data) if img == nil { return nil, fmt.Errorf("win32: unsupported DIB format") } return img, nil } // decodeDIB decodes a BITMAPINFOHEADER-based DIB blob (no BMP file // header) into an *image.RGBA. Only uncompressed 24-bit and 32-bit // pixel formats are supported, which covers what the Windows snipping // stack produces. func decodeDIB(dib []byte) image.Image { if len(dib) < 40 { return nil } headerSize := int(readU32(dib, 0)) width := int(int32(readU32(dib, 4))) height := int(int32(readU32(dib, 8))) bitCount := int(readU16(dib, 14)) compression := int(readU32(dib, 16)) if width <= 0 || height == 0 { return nil } if compression != 0 || (bitCount != 24 && bitCount != 32) { return nil } absH := height topDown := false if absH < 0 { absH = -absH topDown = true } rowBytes := ((bitCount*width + 31) / 32) * 4 pixelOffset := headerSize // BITMAPV4/V5 headers may include color masks immediately after, but // since we only accept BI_RGB, headerSize already points past them. if len(dib) < pixelOffset+rowBytes*absH { return nil } img := image.NewRGBA(image.Rect(0, 0, width, absH)) for y := 0; y < absH; y++ { var srcY int if topDown { srcY = y } else { srcY = absH - 1 - y } row := dib[pixelOffset+srcY*rowBytes:] for x := 0; x < width; x++ { off := x * (bitCount / 8) b := row[off] g := row[off+1] r := row[off+2] var a byte = 0xff if bitCount == 32 { // CF_DIB normally writes 0 in the alpha slot; treat 0 // as opaque to avoid an invisible bitmap. ax := row[off+3] if ax != 0 { a = ax } } img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a}) } } return img } func readU16(b []byte, off int) uint16 { return uint16(b[off]) | uint16(b[off+1])<<8 } func readU32(b []byte, off int) uint32 { return uint32(b[off]) | uint32(b[off+1])<<8 | uint32(b[off+2])<<16 | uint32(b[off+3])<<24 }