Skip to content

Instantly share code, notes, and snippets.

@JamesHopbourn
Last active August 1, 2025 05:17
Show Gist options
  • Select an option

  • Save JamesHopbourn/2f0548d8ca0c48bcf301a5eb32c86fa7 to your computer and use it in GitHub Desktop.

Select an option

Save JamesHopbourn/2f0548d8ca0c48bcf301a5eb32c86fa7 to your computer and use it in GitHub Desktop.

imgcopy & imgpaste for Windows

Windows版本的图像剪贴板管理工具,兼容Unix管道哲学,支持与macOS版本跨平台传输。

项目简介

本项目提供两个C++编写的Windows CLI工具:

  • imgcopy.exe:将图片文件或标准输入的图像数据复制到系统剪贴板
  • imgpaste.exe:从系统剪贴板中获取图像数据并以PNG格式输出到标准输出

特性

  • 跨平台兼容:输出标准化PNG格式,支持与macOS版本无缝传输
  • Unix管道友好:完全支持stdin/stdout,遵循Unix工具设计哲学
  • 二进制安全:正确处理二进制数据流,避免Windows命令行文本转换问题
  • 色彩空间标准化:统一使用sRGB色彩空间,确保跨平台显示一致性
  • 透明度支持:完整保留PNG透明度信息

构建要求

  • Windows 10/11
  • Visual Studio 2019 或更新版本
  • CMake 3.10+
  • Windows SDK

编译安装

# 克隆或下载源码后
.\build.bat

# 或手动编译
mkdir build
cd build
cmake .. -G "Visual Studio 16 2019" -A x64
cmake --build . --config Release

编译完成后可将可执行文件复制到系统PATH:

copy build\Release\imgcopy.exe C:\Windows\System32\
copy build\Release\imgpaste.exe C:\Windows\System32\

使用示例

基础用法

# 复制本地文件到剪贴板
imgcopy.exe image.png

# 从标准输入复制图像
type image.jpg | imgcopy.exe -
curl https://example.com/image.png | imgcopy.exe -

# 从剪贴板提取图像
imgpaste.exe > output.png

跨平台传输

# Windows -> macOS
imgpaste.exe | ssh user@macbook.local "imgcopy -"

# macOS -> Windows  
ssh user@macbook.local "imgpaste" | imgcopy.exe -

高级用法

# 图像处理管道
imgpaste.exe | some-image-processor.exe | imgcopy.exe -

# 与API集成 
imgpaste.exe | base64 -w 0 | curl -X POST -d @- api-endpoint

# 批量处理
for %f in (*.jpg) do imgcopy.exe "%f" && pause

跨平台兼容性

本工具与macOS版本完全兼容:

  • 格式统一:输出标准PNG格式
  • 色彩一致:使用sRGB色彩空间
  • 元数据清理:移除平台特定信息
  • 二进制安全:通过SSH等传输保持数据完整性

技术实现

  • 使用GDI+进行图像处理
  • 标准Windows剪贴板API (CF_BITMAP)
  • 二进制模式stdin/stdout处理
  • RAII管理GDI+资源
  • Unicode文件路径支持

故障排除

编译错误

  • 确保安装了Windows SDK
  • 检查Visual Studio版本兼容性

运行时错误

  • 确保系统有足够内存处理大图像
  • 检查文件路径是否正确(支持Unicode)

跨平台传输问题

  • 确保SSH连接使用二进制模式
  • 检查网络传输是否完整

与macOS版本对比

功能 Windows版本 macOS版本
基础功能 ✅ 完全兼容 ✅ 原版
管道支持 ✅ 完全支持 ✅ 原生支持
跨平台传输 ✅ PNG标准化 ✅ 原生PNG
透明度 ✅ 完整支持 ✅ 完整支持
构建复杂度 🟡 需要VS 🟢 仅需Swift

许可证

本项目遵循MIT许可证,与原macOS版本保持一致。

imgcopy & imgpaste

通过命令行复制与粘贴图片到 macOS 剪贴板的 Swift 工具集。

项目简介

本项目提供两个 Swift 编写的 CLI 工具:

  • imgcopy:将图片文件或标准输入的图像数据复制到系统剪贴板。
  • imgpaste:从系统剪贴板中获取图像数据并以数据流的形式输出。

适用于在 macOS 上进行图像处理、脚本自动化或提升终端工作效率的场景。

使用场景举例

  • 将本地或网络图片快速复制到剪贴板,用于粘贴到微信、Slack、浏览器等。
  • 编写自动化截图、上传、分享脚本。
  • 从剪贴板提取图片做图像识别、OCR 或二维码扫描处理。
  • 在本机与远程 Mac 之间通过 SSH 跨设备复制图像。

安装方法

确保你已安装 Swift 编译器(Xcode Command Line Tools):

xcode-select --install

编译方式:

./build.sh

编译完成后会生成两个可执行文件:

  • imgcopy
  • imgpaste

可将它们复制到系统 PATH 中:

sudo cp imgcopy imgpaste /usr/local/bin/

使用示例

以下是常见用法示例,展示如何结合 imgcopyimgpaste 进行图像复制与提取操作。


1. 通过标准输入将本地 JPEG 文件复制到剪贴板

cat result.jpeg | imgcopy -

等价于 imgcopy result.jpeg,适用于需要从管道中传输图像数据的场景。


2. 直接将本地 PNG 文件复制到剪贴板

imgcopy result.png

适用于常规图像文件快速复制到剪贴板。


3. 将在线图片通过 URL 下载后直接复制到剪贴板

curl 'https://images.unsplash.com/photo-1569158049406-6dc6f71ccd48' | imgcopy -

无需保存图片,直接将网络图片复制到剪贴板。


4. 从剪贴板中粘贴图像并保存为 PNG 文件

imgpaste > result.png

将当前剪贴板中的图像保存到 result.png 文件中。


5. 从剪贴板中提取图像并进行二维码识别

imgpaste | zbarimg -

适用于扫码类脚本,从剪贴板中提取图像后直接分析二维码/条形码。


6. 从本机剪贴板读取图像并通过 SSH 复制到远程主机的剪贴板

imgpaste | ssh username@hostname.local '/usr/local/bin/imgcopy -'

7. 从其他地方复制的图片使用 base64 编码提供给 API 使用

imgpaste | base64 | pbcopy
imgpaste|base64 -w 0| jq -Rs '{"model":"gpt-4o","messages":[{"role":"user","content":[{"type":"text","text":"请识别这张图片的内容"},{"type":"image_url","image_url":{"url":("data:image/jpeg;base64," + .)}}]}]}'|http https://api.openai.com/v1/chat/completions "Authorization:Bearer $OPENAI_API_KEY"|jq '.choices[0].message.content'
{
  "id": "chatcmpl-Bwh3QbzqhHaOTMe7WSEBqcSVjQOaK",
  "object": "chat.completion",
  "created": 1753328224,
  "model": "gpt-4o-2024-08-06",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "这是一个表情符号风格的插图,展示了一个人坐在一台带有苹果标志的笔记本电脑前面。这个人有深色头发和大眼睛,正在注视着电脑屏幕。",
        "refusal": null,
        "annotations": []
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 270,
    "completion_tokens": 53,
    "total_tokens": 323,
    "prompt_tokens_details": {
      "cached_tokens": 0,
      "audio_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0,
      "audio_tokens": 0,
      "accepted_prediction_tokens": 0,
      "rejected_prediction_tokens": 0
    }
  },
  "service_tier": "default",
  "system_fingerprint": "fp_a288987b44"
}
imgpaste|base64 -w 0| jq -Rs '{"model":"gpt-4o","messages":[{"role":"user","content":[{"type":"text","text":"请识 别这张图片的内容"},{"type":"image_url","image_url":{"url":("data:image/jpeg;base64," + .)}}]}]}'|http https://api.openai.com/v1/chat/completions "Authorization:Bearer $OPENAI_API_KEY"|jq '.choices[0].message.content'
"这是一个表情符号样式的图像,显示一个人正在使用一台苹果笔记本电脑。这个人物的设计是卡通化的,似乎集中精力于屏幕。"

实现跨设备复制图片到远程 macOS 主机的剪贴板(需双方都安装本工具)。

@echo off
echo Building imgtools for Windows...
if not exist build mkdir build
cd build
cmake .. -G "Visual Studio 16 2019" -A x64
if %errorlevel% neq 0 (
echo CMake configuration failed
exit /b 1
)
cmake --build . --config Release
if %errorlevel% neq 0 (
echo Build failed
exit /b 1
)
echo.
echo Build completed successfully!
echo Executables are in build\Release\
echo.
echo To install to system PATH:
echo copy build\Release\imgcopy.exe C:\Windows\System32\
echo copy build\Release\imgpaste.exe C:\Windows\System32\
swiftc -o imgcopy imgcopy.swift
swiftc -o imgpaste imgpaste.swift
cmake_minimum_required(VERSION 3.10)
project(imgtools)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Windows specific settings
if(WIN32)
# Add Windows libraries
find_library(GDIPLUS_LIB gdiplus)
# imgcopy executable
add_executable(imgcopy imgcopy.cpp)
target_link_libraries(imgcopy gdiplus ole32 oleaut32)
# imgpaste executable
add_executable(imgpaste imgpaste.cpp)
target_link_libraries(imgpaste gdiplus ole32 oleaut32)
# Set subsystem to console
set_target_properties(imgcopy PROPERTIES
LINK_FLAGS "/SUBSYSTEM:CONSOLE"
)
set_target_properties(imgpaste PROPERTIES
LINK_FLAGS "/SUBSYSTEM:CONSOLE"
)
# Enable Unicode
target_compile_definitions(imgcopy PRIVATE UNICODE _UNICODE)
target_compile_definitions(imgpaste PRIVATE UNICODE _UNICODE)
else()
message(FATAL_ERROR "This project is designed for Windows only")
endif()
# Compiler warnings
if(MSVC)
target_compile_options(imgcopy PRIVATE /W4)
target_compile_options(imgpaste PRIVATE /W4)
else()
target_compile_options(imgcopy PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(imgpaste PRIVATE -Wall -Wextra -Wpedantic)
endif()
#include <windows.h>
#include <gdiplus.h>
#include <iostream>
#include <vector>
#include <io.h>
#include <fcntl.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
using namespace std;
class GdiplusRAII {
private:
ULONG_PTR gdiplusToken;
public:
GdiplusRAII() {
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
}
~GdiplusRAII() {
GdiplusShutdown(gdiplusToken);
}
};
bool CopyImageToClipboard(Bitmap* bitmap) {
if (!bitmap) return false;
HBITMAP hBitmap;
if (bitmap->GetHBITMAP(Color(255, 255, 255), &hBitmap) != Ok) {
return false;
}
if (!OpenClipboard(NULL)) {
DeleteObject(hBitmap);
return false;
}
EmptyClipboard();
bool success = SetClipboardData(CF_BITMAP, hBitmap) != NULL;
CloseClipboard();
if (!success) {
DeleteObject(hBitmap);
}
return success;
}
Bitmap* LoadImageFromFile(const wstring& filePath) {
return new Bitmap(filePath.c_str());
}
Bitmap* LoadImageFromStdin() {
_setmode(_fileno(stdin), _O_BINARY);
vector<BYTE> buffer;
const size_t chunkSize = 4096;
BYTE chunk[chunkSize];
while (!cin.eof()) {
cin.read(reinterpret_cast<char*>(chunk), chunkSize);
streamsize bytesRead = cin.gcount();
if (bytesRead > 0) {
buffer.insert(buffer.end(), chunk, chunk + bytesRead);
}
}
if (buffer.empty()) return nullptr;
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, buffer.size());
if (!hMem) return nullptr;
void* pMem = GlobalLock(hMem);
if (!pMem) {
GlobalFree(hMem);
return nullptr;
}
memcpy(pMem, buffer.data(), buffer.size());
GlobalUnlock(hMem);
IStream* pStream = nullptr;
if (CreateStreamOnHGlobal(hMem, TRUE, &pStream) != S_OK) {
GlobalFree(hMem);
return nullptr;
}
Bitmap* bitmap = new Bitmap(pStream);
pStream->Release();
return bitmap;
}
wstring StringToWString(const string& str) {
int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0);
wstring wstr(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &wstr[0], len);
return wstr;
}
void PrintUsage() {
cout << "Usage:\n\n";
cout << "Copy file to clipboard:\n";
cout << " imgcopy.exe path/to/image.png\n\n";
cout << "Copy stdin to clipboard:\n";
cout << " type image.png | imgcopy.exe -\n";
cout << " curl https://example.com/image.jpg | imgcopy.exe -\n";
}
int main(int argc, char* argv[]) {
if (argc < 2) {
PrintUsage();
return 1;
}
GdiplusRAII gdiplusInit;
string arg = argv[1];
Bitmap* bitmap = nullptr;
if (arg == "-") {
bitmap = LoadImageFromStdin();
} else {
wstring filePath = StringToWString(arg);
bitmap = LoadImageFromFile(filePath);
}
if (!bitmap || bitmap->GetLastStatus() != Ok) {
cerr << "Failed to load image." << endl;
delete bitmap;
return 1;
}
bool success = CopyImageToClipboard(bitmap);
delete bitmap;
if (!success) {
cerr << "Failed to copy image to clipboard." << endl;
return 1;
}
return 0;
}
import Cocoa
func copyToClipboard(from path: String) -> Bool {
var image: NSImage?
if path == "-" {
// 从标准输入读取
let inputData = FileHandle.standardInput.readDataToEndOfFile()
image = NSImage(data: inputData)
} else {
image = NSImage(contentsOfFile: path)
}
guard let validImage = image else {
print("Failed to load image.")
return false
}
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
return pasteboard.writeObjects([validImage])
}
// MARK: - Main
if CommandLine.argc < 2 {
print("""
Usage:
Copy file to clipboard:
./imgcopy path/to/image.png
Copy stdin to clipboard:
cat image.png | ./imgcopy -
""")
exit(EXIT_FAILURE)
}
let path = CommandLine.arguments[1]
let success = copyToClipboard(from: path)
exit(success ? EXIT_SUCCESS : EXIT_FAILURE)
#include <windows.h>
#include <gdiplus.h>
#include <iostream>
#include <vector>
#include <io.h>
#include <fcntl.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
using namespace std;
class GdiplusRAII {
private:
ULONG_PTR gdiplusToken;
public:
GdiplusRAII() {
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
}
~GdiplusRAII() {
GdiplusShutdown(gdiplusToken);
}
};
int GetEncoderClsid(const WCHAR* format, CLSID* pClsid) {
UINT num = 0;
UINT size = 0;
GetImageEncodersSize(&num, &size);
if (size == 0) return -1;
ImageCodecInfo* pImageCodecInfo = (ImageCodecInfo*)(malloc(size));
if (pImageCodecInfo == NULL) return -1;
GetImageEncoders(num, size, pImageCodecInfo);
for (UINT j = 0; j < num; ++j) {
if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0) {
*pClsid = pImageCodecInfo[j].Clsid;
free(pImageCodecInfo);
return j;
}
}
free(pImageCodecInfo);
return -1;
}
bool SaveBitmapToPngStream(Bitmap* bitmap, IStream* stream) {
if (!bitmap || !stream) return false;
CLSID pngClsid;
if (GetEncoderClsid(L"image/png", &pngClsid) == -1) {
return false;
}
return bitmap->Save(stream, &pngClsid, NULL) == Ok;
}
bool PasteImageFromClipboard() {
if (!OpenClipboard(NULL)) {
return false;
}
HBITMAP hBitmap = (HBITMAP)GetClipboardData(CF_BITMAP);
if (!hBitmap) {
CloseClipboard();
return false;
}
Bitmap* bitmap = new Bitmap(hBitmap, NULL);
CloseClipboard();
if (!bitmap || bitmap->GetLastStatus() != Ok) {
delete bitmap;
return false;
}
// 标准化为sRGB色彩空间
Bitmap* srgbBitmap = bitmap->Clone(0, 0, bitmap->GetWidth(), bitmap->GetHeight(), PixelFormat32bppARGB);
delete bitmap;
if (!srgbBitmap) {
return false;
}
// 创建内存流
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, 0);
if (!hMem) {
delete srgbBitmap;
return false;
}
IStream* pStream = nullptr;
if (CreateStreamOnHGlobal(hMem, TRUE, &pStream) != S_OK) {
GlobalFree(hMem);
delete srgbBitmap;
return false;
}
// 保存为PNG格式
bool success = SaveBitmapToPngStream(srgbBitmap, pStream);
delete srgbBitmap;
if (!success) {
pStream->Release();
return false;
}
// 获取流数据
LARGE_INTEGER zero = {};
pStream->Seek(zero, STREAM_SEEK_SET, NULL);
STATSTG statstg;
if (pStream->Stat(&statstg, STATFLAG_NONAME) != S_OK) {
pStream->Release();
return false;
}
SIZE_T dataSize = statstg.cbSize.LowPart;
vector<BYTE> buffer(dataSize);
ULONG bytesRead;
if (pStream->Read(buffer.data(), dataSize, &bytesRead) != S_OK || bytesRead != dataSize) {
pStream->Release();
return false;
}
pStream->Release();
// 设置stdout为二进制模式
_setmode(_fileno(stdout), _O_BINARY);
// 输出PNG数据到stdout
cout.write(reinterpret_cast<const char*>(buffer.data()), dataSize);
cout.flush();
return true;
}
int main() {
GdiplusRAII gdiplusInit;
if (!PasteImageFromClipboard()) {
cerr << "No valid image found in clipboard." << endl;
return 1;
}
return 0;
}
import Cocoa
func pasteFromClipboard() -> Data? {
let pasteboard = NSPasteboard.general
guard let image = pasteboard.readObjects(forClasses: [NSImage.self], options: nil)?.first as? NSImage else {
return nil
}
guard let tiffData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffData),
let pngData = bitmap.representation(using: .png, properties: [:]) else {
return nil
}
return pngData
}
// MARK: - Main
if let imageData = pasteFromClipboard() {
FileHandle.standardOutput.write(imageData)
exit(EXIT_SUCCESS)
} else {
fputs("No valid image found in clipboard.\n", stderr)
exit(EXIT_FAILURE)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment