winclip 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f6d5df273deafae1f60beee563bf2c2e98b1485f858be569b2201e35eedf91c1
4
+ data.tar.gz: acd8ef65e6cc2b4435ddbc36be9af15fdc4cf8717f05f39d961319bf32132175
5
+ SHA512:
6
+ metadata.gz: 9570a2bc063aa31b16d252d04465a5d03deea83e7d8f9e0a86a7c2ce1c9e38801038e6d1e9d2ab12d3f405703d23bb9f467814cd4f546549a8ca86ab51bcdcb4
7
+ data.tar.gz: 80080336d80b9de4d1630f6c47a3a6b99d5ed1eb4be6b19f64b12a6c4d046f5e6d8381268bce45a9813f751a006de28cac0d3cebc8e4682bc2ccd1fa85acae9d
data/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-05-30
10
+
11
+ ### Added
12
+ - `Winclip.text` / `Winclip.text=` — get/set Unicode clipboard text (UTF-8).
13
+ - `Winclip.image` / `Winclip.image=` — get/set the clipboard image as PNG bytes
14
+ (CF_DIBV5 + CF_DIB via WIC; handles 24/32bpp, top-down/bottom-up on read).
15
+ - `Winclip.files` / `Winclip.files=` — get/set the clipboard file list (CF_HDROP).
16
+ - `Winclip.copy` / `Winclip.paste` — text convenience aliases.
17
+ - `Winclip.clear` — empty the clipboard.
18
+ - `Winclip.formats` — list available clipboard format names.
19
+ - `Winclip.has_text?` / `has_image?` / `has_files?` and `available?(fmt)`.
20
+
21
+ ### Security / robustness
22
+ - `Winclip.image` fully validates an untrusted clipboard DIB (minimum header
23
+ size, header size, and that the claimed pixels fit the actual handle) before
24
+ reading, and bounds allocation against the real data — preventing OOB reads
25
+ and runaway allocations from a malformed/hostile clipboard image. No C++
26
+ exception can unwind while the clipboard is held open. An undecodable image
27
+ returns `nil` rather than raising. Image size math is 64-bit (no overflow).
28
+ - `available?(name)` resolves custom format names without registering them.
29
+
30
+ [Unreleased]: https://github.com/main-path/winclip/compare/v0.1.0...HEAD
31
+ [0.1.0]: https://github.com/main-path/winclip/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ned
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # winclip
2
+
3
+ **Read and write the Windows clipboard from Ruby: text, images, and file lists.**
4
+
5
+ `winclip` is a native Windows clipboard library. It gets and sets:
6
+
7
+ - **text** — Unicode, as UTF-8 Strings;
8
+ - **images** — as **PNG bytes** (converted to/from `CF_DIB`/`CF_DIBV5` via WIC);
9
+ - **file lists** — as arrays of paths (`CF_HDROP`).
10
+
11
+ The PNG image interface pairs directly with [windraw](https://rubygems.org/gems/windraw) —
12
+ draw something and drop it on the clipboard, or grab a copied screenshot and save it.
13
+
14
+ ## Requirements
15
+
16
+ - **Windows** with a native **MSVC (mswin)** Ruby (`x64-mswin64`). Not supported
17
+ on MinGW/UCRT Ruby (the `extconf.rb` will say so).
18
+ - Visual Studio 2017+ / Build Tools with the **Desktop development with C++**
19
+ workload. Building uses [`vcvars`](https://rubygems.org/gems/vcvars) to load
20
+ the toolchain automatically — no Developer Command Prompt needed.
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ gem install winclip
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```ruby
31
+ require "winclip"
32
+
33
+ # --- text ---
34
+ Winclip.text = "héllo ✓ 日本"
35
+ Winclip.text # => "héllo ✓ 日本" (UTF-8)
36
+ Winclip.copy("quick") # alias for text=
37
+ Winclip.paste # alias for text
38
+
39
+ # --- images (PNG bytes) ---
40
+ Winclip.image = File.binread("logo.png")
41
+ File.binwrite("pasted.png", Winclip.image) if Winclip.has_image?
42
+
43
+ # straight from windraw:
44
+ require "windraw"
45
+ Winclip.image = Windraw.surface(200, 80) { |c| c.clear("#1e1e2e"); c.text("hi", 10, 20, color: "#fff") }.to_png
46
+
47
+ # --- file lists ---
48
+ Winclip.files = ["C:/a.txt", "C:/b.txt"] # also accepts a single path String
49
+ Winclip.files # => ["C:/a.txt", "C:/b.txt"]
50
+
51
+ # --- queries & management ---
52
+ Winclip.has_text? # => true / false
53
+ Winclip.has_image? # => true / false
54
+ Winclip.has_files? # => true / false
55
+ Winclip.formats # => ["CF_UNICODETEXT", "CF_TEXT", "CF_LOCALE", ...]
56
+ Winclip.available?(:image) # => true / false
57
+ Winclip.available?("PNG") # registered format by name
58
+ Winclip.available?("CF_HDROP") # standard format by name
59
+ Winclip.clear # empty the clipboard
60
+ ```
61
+
62
+ All getters return `nil` when that kind of content isn't on the clipboard, so
63
+ you can write `Winclip.text or abort "nothing copied"`.
64
+
65
+ ## Notes
66
+
67
+ - Images are exchanged as **PNG bytes** (binary `ASCII-8BIT` Strings). On set,
68
+ winclip publishes both `CF_DIBV5` (alpha-aware) and `CF_DIB` for the widest
69
+ app compatibility; on get it reads `CF_DIBV5`/`CF_DIB` (24- or 32-bit) and
70
+ encodes PNG. Palette (1/4/8-bit) DIBs are not decoded.
71
+ - Clipboard work happens on the calling thread; do clipboard operations from one
72
+ thread.
73
+
74
+ ## License
75
+
76
+ [MIT](LICENSE.txt).
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # extconf.rb for the winclip C++ extension (Windows clipboard via Win32 + WIC).
4
+
5
+ require "mkmf"
6
+
7
+ unless RbConfig::CONFIG["target_os"] =~ /mswin/
8
+ abort <<~MSG
9
+ winclip requires a native Windows MSVC (mswin) Ruby — it uses the Win32
10
+ clipboard APIs and WIC (cl.exe). Your Ruby is "#{RbConfig::CONFIG['arch']}".
11
+ MSG
12
+ end
13
+
14
+ # C++ (WIC/COM uses IID_PPV_ARGS, BITMAPV5HEADER, std::vector); enable exceptions.
15
+ $CXXFLAGS << " -EHsc"
16
+
17
+ # System import libs (bare "NAME.lib" tokens on mswin):
18
+ # user32 - clipboard APIs (Open/Empty/Set/Get/Enum/IsAvailable)
19
+ # shell32 - DragQueryFileW / DROPFILES (CF_HDROP)
20
+ # windowscodecs - WIC (PNG <-> DIB for images)
21
+ # ole32 - COM (CoInitializeEx / CoCreateInstance)
22
+ # shlwapi - SHCreateMemStream (in-memory PNG streams)
23
+ $libs = [$libs, "user32.lib", "shell32.lib", "windowscodecs.lib",
24
+ "ole32.lib", "shlwapi.lib"].join(" ")
25
+
26
+ create_makefile("winclip/winclip")
@@ -0,0 +1,576 @@
1
+ /*
2
+ * winclip — Windows clipboard access for Ruby: text, images (PNG), and file
3
+ * lists, get and set.
4
+ *
5
+ * Text -> CF_UNICODETEXT (UTF-8 <-> UTF-16).
6
+ * Files -> CF_HDROP (DROPFILES + double-NUL UTF-16 path list).
7
+ * Image -> CF_DIBV5 + CF_DIB on set; CF_DIBV5/CF_DIB -> PNG on get, via WIC.
8
+ *
9
+ * Discipline (verified): the clipboard is ALWAYS closed before any rb_raise
10
+ * (rb_raise longjmps and would otherwise leave the clipboard locked for every
11
+ * other app). Win32 work happens in helper functions that return status codes
12
+ * and never call into Ruby; the Ruby-facing wrappers raise only after the
13
+ * clipboard is closed and C buffers are freed. After a successful
14
+ * SetClipboardData the system owns the HGLOBAL — we only GlobalFree on failure.
15
+ *
16
+ * Build: <ruby.h> before <windows.h>; C++ with -EHsc; never name a var OUT/IN.
17
+ * Link: user32 shell32 windowscodecs ole32 shlwapi.
18
+ */
19
+
20
+ #include <ruby.h>
21
+ #include <ruby/encoding.h>
22
+
23
+ #define WIN32_LEAN_AND_MEAN
24
+ #include <windows.h>
25
+ #include <shlobj.h> /* DROPFILES */
26
+ #include <shellapi.h> /* DragQueryFileW */
27
+ #include <wincodec.h> /* WIC */
28
+ #include <shlwapi.h> /* SHCreateMemStream */
29
+ #include <objbase.h> /* CoInitializeEx */
30
+
31
+ #include <vector>
32
+ #include <cstring>
33
+ #include <cstdint>
34
+
35
+ static VALUE eWinclipError; /* Winclip::Error */
36
+
37
+ /* ---------- UTF-8 <-> UTF-16 (LocalAlloc'd results) ---------------------- */
38
+
39
+ static wchar_t *utf8_to_utf16(const char *utf8, int len /* -1 = NUL-terminated */) {
40
+ int need = MultiByteToWideChar(CP_UTF8, 0, utf8, len, NULL, 0);
41
+ if (need <= 0) return NULL;
42
+ wchar_t *w = (wchar_t *)LocalAlloc(LPTR, (size_t)need * sizeof(wchar_t));
43
+ if (!w) return NULL;
44
+ if (MultiByteToWideChar(CP_UTF8, 0, utf8, len, w, need) <= 0) { LocalFree(w); return NULL; }
45
+ return w;
46
+ }
47
+
48
+ static char *utf16_to_utf8(const wchar_t *w, int len /* -1 = NUL-terminated */) {
49
+ int need = WideCharToMultiByte(CP_UTF8, 0, w, len, NULL, 0, NULL, NULL);
50
+ if (need <= 0) return NULL;
51
+ char *s = (char *)LocalAlloc(LPTR, (size_t)need);
52
+ if (!s) return NULL;
53
+ if (WideCharToMultiByte(CP_UTF8, 0, w, len, s, need, NULL, NULL) <= 0) { LocalFree(s); return NULL; }
54
+ return s;
55
+ }
56
+
57
+ /* OpenClipboard with a short retry loop (another app may briefly hold it). */
58
+ static int clip_open_retry(void) {
59
+ for (int i = 0; i < 20; i++) { /* ~200 ms worst case */
60
+ if (OpenClipboard(NULL)) return 1;
61
+ Sleep(10);
62
+ }
63
+ return 0;
64
+ }
65
+
66
+ /* ---------- text -------------------------------------------------------- */
67
+
68
+ /* 1 ok / 0 fail. */
69
+ static int clip_set_text(const wchar_t *w) {
70
+ size_t bytes = (wcslen(w) + 1) * sizeof(wchar_t); /* include NUL */
71
+ HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
72
+ if (!hMem) return 0;
73
+ void *dst = GlobalLock(hMem);
74
+ if (!dst) { GlobalFree(hMem); return 0; }
75
+ memcpy(dst, w, bytes);
76
+ GlobalUnlock(hMem);
77
+ if (!clip_open_retry()) { GlobalFree(hMem); return 0; }
78
+ EmptyClipboard();
79
+ if (SetClipboardData(CF_UNICODETEXT, hMem) == NULL) {
80
+ GlobalFree(hMem); CloseClipboard(); return 0; /* failure: we own it */
81
+ }
82
+ CloseClipboard(); /* success: system owns it */
83
+ return 1;
84
+ }
85
+
86
+ /* LocalAlloc'd UTF-8 (caller LocalFree) or NULL if absent/failure. */
87
+ static char *clip_get_text(void) {
88
+ if (!IsClipboardFormatAvailable(CF_UNICODETEXT)) return NULL;
89
+ if (!clip_open_retry()) return NULL;
90
+ HANDLE h = GetClipboardData(CF_UNICODETEXT); /* clipboard-owned */
91
+ char *utf8 = NULL;
92
+ if (h) {
93
+ wchar_t *w = (wchar_t *)GlobalLock(h);
94
+ if (w) { utf8 = utf16_to_utf8(w, -1); GlobalUnlock(h); }
95
+ }
96
+ CloseClipboard();
97
+ return utf8;
98
+ }
99
+
100
+ /* ---------- files (CF_HDROP) -------------------------------------------- */
101
+
102
+ static int clip_set_files(const wchar_t **wp, int count) {
103
+ size_t list_cch = 1; /* trailing extra NUL */
104
+ for (int i = 0; i < count; i++) list_cch += wcslen(wp[i]) + 1;
105
+ size_t bytes = sizeof(DROPFILES) + list_cch * sizeof(wchar_t);
106
+ HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
107
+ if (!hMem) return 0;
108
+ BYTE *base = (BYTE *)GlobalLock(hMem);
109
+ if (!base) { GlobalFree(hMem); return 0; }
110
+ DROPFILES *df = (DROPFILES *)base;
111
+ df->pFiles = sizeof(DROPFILES);
112
+ df->pt.x = 0; df->pt.y = 0;
113
+ df->fNC = FALSE;
114
+ df->fWide = TRUE;
115
+ wchar_t *list = (wchar_t *)(base + sizeof(DROPFILES));
116
+ size_t pos = 0;
117
+ for (int i = 0; i < count; i++) {
118
+ size_t n = wcslen(wp[i]) + 1;
119
+ memcpy(list + pos, wp[i], n * sizeof(wchar_t));
120
+ pos += n;
121
+ }
122
+ list[pos] = L'\0'; /* double-NUL terminate */
123
+ GlobalUnlock(hMem);
124
+ if (!clip_open_retry()) { GlobalFree(hMem); return 0; }
125
+ EmptyClipboard();
126
+ if (SetClipboardData(CF_HDROP, hMem) == NULL) { GlobalFree(hMem); CloseClipboard(); return 0; }
127
+ CloseClipboard();
128
+ return 1;
129
+ }
130
+
131
+ /* On success *out = LocalAlloc'd array of LocalAlloc'd UTF-8 strings; returns
132
+ * count (>=0). Returns -1 when no CF_HDROP / on failure. */
133
+ static int clip_get_files(char ***out) {
134
+ *out = NULL;
135
+ if (!IsClipboardFormatAvailable(CF_HDROP)) return -1;
136
+ if (!clip_open_retry()) return -1;
137
+ HANDLE h = GetClipboardData(CF_HDROP);
138
+ if (!h) { CloseClipboard(); return -1; }
139
+ HDROP hDrop = (HDROP)h;
140
+ UINT n = DragQueryFileW(hDrop, 0xFFFFFFFF, NULL, 0);
141
+ char **arr = (char **)LocalAlloc(LPTR, (size_t)(n ? n : 1) * sizeof(char *));
142
+ if (!arr) { CloseClipboard(); return -1; }
143
+ for (UINT i = 0; i < n; i++) {
144
+ UINT cch = DragQueryFileW(hDrop, i, NULL, 0);
145
+ wchar_t *wbuf = (wchar_t *)LocalAlloc(LPTR, (size_t)(cch + 1) * sizeof(wchar_t));
146
+ if (!wbuf) { for (UINT j = 0; j < i; j++) LocalFree(arr[j]); LocalFree(arr); CloseClipboard(); return -1; }
147
+ DragQueryFileW(hDrop, i, wbuf, cch + 1);
148
+ arr[i] = utf16_to_utf8(wbuf, -1);
149
+ LocalFree(wbuf);
150
+ if (!arr[i]) { for (UINT j = 0; j < i; j++) LocalFree(arr[j]); LocalFree(arr); CloseClipboard(); return -1; }
151
+ }
152
+ CloseClipboard();
153
+ *out = arr;
154
+ return (int)n;
155
+ }
156
+
157
+ /* ---------- image (WIC) ------------------------------------------------- */
158
+
159
+ static HRESULT png_to_bgra(const BYTE *png, size_t pnglen,
160
+ std::vector<BYTE> &outBgra, UINT &w, UINT &h) {
161
+ IStream *stream = SHCreateMemStream(png, (UINT)pnglen);
162
+ if (!stream) return E_OUTOFMEMORY;
163
+ IWICImagingFactory *factory = NULL; IWICBitmapDecoder *decoder = NULL;
164
+ IWICBitmapFrameDecode *frame = NULL; IWICFormatConverter *conv = NULL;
165
+ HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER,
166
+ IID_PPV_ARGS(&factory));
167
+ if (SUCCEEDED(hr)) hr = factory->CreateDecoderFromStream(stream, NULL, WICDecodeMetadataCacheOnDemand, &decoder);
168
+ if (SUCCEEDED(hr)) hr = decoder->GetFrame(0, &frame);
169
+ if (SUCCEEDED(hr)) hr = factory->CreateFormatConverter(&conv);
170
+ if (SUCCEEDED(hr)) hr = conv->Initialize(frame, GUID_WICPixelFormat32bppBGRA,
171
+ WICBitmapDitherTypeNone, NULL, 0.0, WICBitmapPaletteTypeCustom);
172
+ if (SUCCEEDED(hr)) hr = conv->GetSize(&w, &h);
173
+ if (SUCCEEDED(hr)) {
174
+ uint64_t stride64 = (uint64_t)w * 4;
175
+ uint64_t total = stride64 * h;
176
+ if (w == 0 || h == 0 || stride64 > 0xFFFFFFFFu || total > 0xFFFFFFFFu) {
177
+ hr = E_OUTOFMEMORY; /* WIC's CopyPixels uses UINT sizes */
178
+ } else {
179
+ UINT stride = (UINT)stride64;
180
+ outBgra.resize((size_t)total);
181
+ WICRect rc = { 0, 0, (INT)w, (INT)h };
182
+ hr = conv->CopyPixels(&rc, stride, (UINT)outBgra.size(), outBgra.data());
183
+ }
184
+ }
185
+ if (conv) conv->Release();
186
+ if (frame) frame->Release();
187
+ if (decoder) decoder->Release();
188
+ if (factory) factory->Release();
189
+ if (stream) stream->Release();
190
+ return hr;
191
+ }
192
+
193
+ static HRESULT bgra_to_png(const BYTE *bgra, UINT w, UINT h, UINT stride, std::vector<BYTE> &outPng) {
194
+ IWICImagingFactory *factory = NULL; IWICStream *stream = NULL;
195
+ IWICBitmapEncoder *encoder = NULL; IWICBitmapFrameEncode *frame = NULL;
196
+ IStream *memstream = SHCreateMemStream(NULL, 0);
197
+ if (!memstream) return E_OUTOFMEMORY;
198
+ HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER,
199
+ IID_PPV_ARGS(&factory));
200
+ if (SUCCEEDED(hr)) hr = factory->CreateStream(&stream);
201
+ if (SUCCEEDED(hr)) hr = stream->InitializeFromIStream(memstream);
202
+ if (SUCCEEDED(hr)) hr = factory->CreateEncoder(GUID_ContainerFormatPng, NULL, &encoder);
203
+ if (SUCCEEDED(hr)) hr = encoder->Initialize(stream, WICBitmapEncoderNoCache);
204
+ if (SUCCEEDED(hr)) hr = encoder->CreateNewFrame(&frame, NULL);
205
+ if (SUCCEEDED(hr)) hr = frame->Initialize(NULL);
206
+ if (SUCCEEDED(hr)) hr = frame->SetSize(w, h);
207
+ WICPixelFormatGUID fmt = GUID_WICPixelFormat32bppBGRA;
208
+ if (SUCCEEDED(hr)) hr = frame->SetPixelFormat(&fmt);
209
+ if (SUCCEEDED(hr)) {
210
+ uint64_t cb = (uint64_t)stride * h;
211
+ if (cb > 0xFFFFFFFFu) hr = E_OUTOFMEMORY;
212
+ else hr = frame->WritePixels(h, stride, (UINT)cb, (BYTE *)bgra);
213
+ }
214
+ if (SUCCEEDED(hr)) hr = frame->Commit();
215
+ if (SUCCEEDED(hr)) hr = encoder->Commit();
216
+ if (SUCCEEDED(hr)) {
217
+ STATSTG st; memset(&st, 0, sizeof(st));
218
+ hr = memstream->Stat(&st, STATFLAG_NONAME);
219
+ if (SUCCEEDED(hr)) {
220
+ ULONG size = (ULONG)st.cbSize.QuadPart;
221
+ outPng.resize(size);
222
+ LARGE_INTEGER zero; zero.QuadPart = 0;
223
+ memstream->Seek(zero, STREAM_SEEK_SET, NULL);
224
+ ULONG read = 0;
225
+ if (size) hr = memstream->Read(outPng.data(), size, &read);
226
+ if (SUCCEEDED(hr)) outPng.resize(read);
227
+ }
228
+ }
229
+ if (frame) frame->Release();
230
+ if (encoder) encoder->Release();
231
+ if (stream) stream->Release();
232
+ if (factory) factory->Release();
233
+ if (memstream) memstream->Release();
234
+ return hr;
235
+ }
236
+
237
+ /* PNG bytes -> clipboard (sets CF_DIBV5 + CF_DIB). Caller must have COM init'd. */
238
+ static HRESULT clip_set_image(const BYTE *png, size_t pnglen) {
239
+ std::vector<BYTE> bgra; UINT w = 0, h = 0;
240
+ HRESULT hr = png_to_bgra(png, pnglen, bgra, w, h);
241
+ if (FAILED(hr)) return hr;
242
+
243
+ /* Compute the DIB byte size in 64-bit and reject images too large to encode
244
+ * as a clipboard DIB (avoids the w*4*h 32-bit wrap that would corrupt it). */
245
+ uint64_t imgSize64 = (uint64_t)w * 4 * h;
246
+ if (imgSize64 == 0 || imgSize64 + sizeof(BITMAPV5HEADER) > 0xFFFFFFFFu)
247
+ return E_OUTOFMEMORY;
248
+ const DWORD imgSize = (DWORD)imgSize64;
249
+
250
+ HGLOBAL hV5 = GlobalAlloc(GMEM_MOVEABLE, sizeof(BITMAPV5HEADER) + imgSize);
251
+ if (!hV5) return E_OUTOFMEMORY;
252
+ {
253
+ BYTE *p = (BYTE *)GlobalLock(hV5);
254
+ if (!p) { GlobalFree(hV5); return E_OUTOFMEMORY; }
255
+ BITMAPV5HEADER *bi = (BITMAPV5HEADER *)p; memset(bi, 0, sizeof(*bi));
256
+ bi->bV5Size = sizeof(BITMAPV5HEADER);
257
+ bi->bV5Width = (LONG)w; bi->bV5Height = -(LONG)h; /* top-down */
258
+ bi->bV5Planes = 1; bi->bV5BitCount = 32;
259
+ bi->bV5Compression = BI_BITFIELDS; bi->bV5SizeImage = imgSize;
260
+ bi->bV5RedMask = 0x00FF0000; bi->bV5GreenMask = 0x0000FF00;
261
+ bi->bV5BlueMask = 0x000000FF; bi->bV5AlphaMask = 0xFF000000;
262
+ bi->bV5CSType = LCS_sRGB; bi->bV5Intent = LCS_GM_IMAGES;
263
+ memcpy(p + sizeof(BITMAPV5HEADER), bgra.data(), imgSize);
264
+ GlobalUnlock(hV5);
265
+ }
266
+ HGLOBAL hDib = GlobalAlloc(GMEM_MOVEABLE, sizeof(BITMAPINFOHEADER) + imgSize);
267
+ if (!hDib) { GlobalFree(hV5); return E_OUTOFMEMORY; }
268
+ {
269
+ BYTE *p = (BYTE *)GlobalLock(hDib);
270
+ if (!p) { GlobalFree(hV5); GlobalFree(hDib); return E_OUTOFMEMORY; }
271
+ BITMAPINFOHEADER *bi = (BITMAPINFOHEADER *)p; memset(bi, 0, sizeof(*bi));
272
+ bi->biSize = sizeof(BITMAPINFOHEADER);
273
+ bi->biWidth = (LONG)w; bi->biHeight = -(LONG)h; /* top-down */
274
+ bi->biPlanes = 1; bi->biBitCount = 32;
275
+ bi->biCompression = BI_RGB; bi->biSizeImage = imgSize;
276
+ memcpy(p + sizeof(BITMAPINFOHEADER), bgra.data(), imgSize);
277
+ GlobalUnlock(hDib);
278
+ }
279
+ if (!clip_open_retry()) { GlobalFree(hV5); GlobalFree(hDib); return E_FAIL; }
280
+ EmptyClipboard();
281
+ bool ok = true;
282
+ if (!SetClipboardData(CF_DIBV5, hV5)) { ok = false; GlobalFree(hV5); }
283
+ if (!SetClipboardData(CF_DIB, hDib)) { ok = false; GlobalFree(hDib); }
284
+ CloseClipboard();
285
+ return ok ? S_OK : E_FAIL;
286
+ }
287
+
288
+ /* clipboard image -> PNG bytes. Caller must have COM init'd.
289
+ *
290
+ * The DIB is UNTRUSTED data placed by another app, so every field and offset is
291
+ * validated against the real handle size before use, and the pixel-copy region
292
+ * runs inside a try/catch so a std::bad_alloc can never unwind out while the
293
+ * clipboard is held open (which would wedge the clipboard system-wide).
294
+ * Returns E_FAIL for absent / malformed / unsupported (e.g. palettized) DIBs. */
295
+ static HRESULT clip_get_image(std::vector<BYTE> &outPng) {
296
+ if (!clip_open_retry()) return E_FAIL;
297
+ HANDLE h = GetClipboardData(CF_DIBV5);
298
+ if (!h) h = GetClipboardData(CF_DIB);
299
+ if (!h) { CloseClipboard(); return E_FAIL; }
300
+ BYTE *dib = (BYTE *)GlobalLock(h);
301
+ if (!dib) { CloseClipboard(); return E_FAIL; }
302
+ SIZE_T dibSize = GlobalSize(h);
303
+
304
+ HRESULT hr = S_OK;
305
+ std::vector<BYTE> bgra;
306
+ UINT w = 0, hh = 0;
307
+ try {
308
+ /* The block must be at least a BITMAPINFOHEADER before we read fields. */
309
+ if (dibSize < sizeof(BITMAPINFOHEADER)) throw E_FAIL;
310
+
311
+ BITMAPINFOHEADER *bih = (BITMAPINFOHEADER *)dib;
312
+ DWORD biSize = bih->biSize, comp = bih->biCompression;
313
+ LONG rawH = bih->biHeight;
314
+ WORD bpp = bih->biBitCount;
315
+ bool topDown = (rawH < 0);
316
+ w = (UINT)(bih->biWidth < 0 ? -bih->biWidth : bih->biWidth);
317
+ hh = (UINT)(rawH < 0 ? -rawH : rawH);
318
+ if (w == 0 || hh == 0) throw E_FAIL;
319
+ if (biSize < sizeof(BITMAPINFOHEADER) || biSize > dibSize) throw E_FAIL;
320
+
321
+ SIZE_T pixOffset = biSize;
322
+ if (comp == BI_BITFIELDS && biSize == sizeof(BITMAPINFOHEADER)) pixOffset += 3 * sizeof(DWORD);
323
+ if (pixOffset > dibSize) throw E_FAIL;
324
+ BYTE *pixels = dib + pixOffset;
325
+ SIZE_T pixBytes = dibSize - pixOffset;
326
+
327
+ /* Bound the source against the bytes actually present BEFORE allocating,
328
+ * so untrusted dimensions can't drive a huge allocation. */
329
+ uint64_t srcStride = (bpp == 32) ? (uint64_t)w * 4
330
+ : (bpp == 24) ? (((uint64_t)w * 3 + 3) & ~3ull)
331
+ : 0;
332
+ if (srcStride == 0) throw E_FAIL; /* unsupported bpp */
333
+ if (srcStride * hh > (uint64_t)pixBytes) throw E_FAIL; /* claims more than it carries */
334
+
335
+ bgra.resize((size_t)w * hh * 4);
336
+ DWORD ss = (DWORD)srcStride;
337
+ if (bpp == 32) {
338
+ for (UINT y = 0; y < hh; y++) {
339
+ UINT sr = topDown ? y : (hh - 1 - y);
340
+ memcpy(&bgra[(size_t)y * w * 4], pixels + (size_t)sr * ss, (size_t)w * 4);
341
+ }
342
+ } else { /* bpp == 24 */
343
+ for (UINT y = 0; y < hh; y++) {
344
+ UINT sr = topDown ? y : (hh - 1 - y);
345
+ BYTE *srow = pixels + (size_t)sr * ss;
346
+ BYTE *drow = &bgra[(size_t)y * w * 4];
347
+ for (UINT x = 0; x < w; x++) {
348
+ drow[x * 4 + 0] = srow[x * 3 + 0];
349
+ drow[x * 4 + 1] = srow[x * 3 + 1];
350
+ drow[x * 4 + 2] = srow[x * 3 + 2];
351
+ drow[x * 4 + 3] = 255;
352
+ }
353
+ }
354
+ }
355
+ } catch (HRESULT e) {
356
+ hr = e;
357
+ } catch (...) {
358
+ hr = E_OUTOFMEMORY; /* e.g. std::bad_alloc — never escape with clipboard open */
359
+ }
360
+ GlobalUnlock(h);
361
+ CloseClipboard();
362
+ if (FAILED(hr)) return hr;
363
+ return bgra_to_png(bgra.data(), w, hh, w * 4, outPng);
364
+ }
365
+
366
+ /* ---------- format names ------------------------------------------------ */
367
+
368
+ static const char *clip_std_name(UINT f) {
369
+ switch (f) {
370
+ case CF_TEXT: return "CF_TEXT";
371
+ case CF_BITMAP: return "CF_BITMAP";
372
+ case CF_METAFILEPICT: return "CF_METAFILEPICT";
373
+ case CF_SYLK: return "CF_SYLK";
374
+ case CF_DIF: return "CF_DIF";
375
+ case CF_TIFF: return "CF_TIFF";
376
+ case CF_OEMTEXT: return "CF_OEMTEXT";
377
+ case CF_DIB: return "CF_DIB";
378
+ case CF_PALETTE: return "CF_PALETTE";
379
+ case CF_PENDATA: return "CF_PENDATA";
380
+ case CF_RIFF: return "CF_RIFF";
381
+ case CF_WAVE: return "CF_WAVE";
382
+ case CF_UNICODETEXT: return "CF_UNICODETEXT";
383
+ case CF_ENHMETAFILE: return "CF_ENHMETAFILE";
384
+ case CF_HDROP: return "CF_HDROP";
385
+ case CF_LOCALE: return "CF_LOCALE";
386
+ case CF_DIBV5: return "CF_DIBV5";
387
+ default: return NULL;
388
+ }
389
+ }
390
+
391
+ /* ====================== Ruby-facing methods ============================= */
392
+
393
+ static VALUE wc_get_text(VALUE self) {
394
+ char *u8 = clip_get_text();
395
+ if (!u8) return Qnil;
396
+ VALUE s = rb_utf8_str_new_cstr(u8);
397
+ LocalFree(u8);
398
+ return s;
399
+ }
400
+
401
+ static VALUE wc_set_text(VALUE self, VALUE str) {
402
+ VALUE u8 = rb_str_export_to_enc(StringValue(str), rb_utf8_encoding());
403
+ wchar_t *w = utf8_to_utf16(RSTRING_PTR(u8), -1);
404
+ if (!w) rb_raise(eWinclipError, "winclip: could not encode text");
405
+ int ok = clip_set_text(w);
406
+ LocalFree(w);
407
+ if (!ok) rb_raise(eWinclipError, "winclip: failed to set clipboard text");
408
+ return Qnil;
409
+ }
410
+
411
+ static VALUE wc_get_files(VALUE self) {
412
+ char **arr = NULL;
413
+ int n = clip_get_files(&arr);
414
+ if (n < 0) return Qnil;
415
+ VALUE rary = rb_ary_new_capa(n);
416
+ for (int i = 0; i < n; i++) {
417
+ rb_ary_push(rary, rb_utf8_str_new_cstr(arr[i]));
418
+ LocalFree(arr[i]);
419
+ }
420
+ LocalFree(arr);
421
+ return rary;
422
+ }
423
+
424
+ static VALUE wc_set_files(VALUE self, VALUE ary) {
425
+ Check_Type(ary, T_ARRAY);
426
+ long n = RARRAY_LEN(ary);
427
+ if (n <= 0) rb_raise(rb_eArgError, "winclip: files list must not be empty");
428
+
429
+ /* Convert all paths to UTF-16 up front (raises here, before any clipboard
430
+ * work or HGLOBAL exists). */
431
+ wchar_t **wp = (wchar_t **)LocalAlloc(LPTR, (size_t)n * sizeof(wchar_t *));
432
+ if (!wp) rb_raise(rb_eNoMemError, "winclip: out of memory");
433
+ int built = 0;
434
+ int failed = 0;
435
+ for (long i = 0; i < n; i++) {
436
+ VALUE e = rb_ary_entry(ary, i);
437
+ if (!RB_TYPE_P(e, T_STRING)) { failed = 1; break; }
438
+ VALUE u8 = rb_str_export_to_enc(e, rb_utf8_encoding());
439
+ wp[i] = utf8_to_utf16(RSTRING_PTR(u8), -1);
440
+ if (!wp[i]) { failed = 1; break; }
441
+ built++;
442
+ }
443
+ int ok = 0;
444
+ if (!failed) ok = clip_set_files((const wchar_t **)wp, (int)n);
445
+ for (int i = 0; i < built; i++) LocalFree(wp[i]);
446
+ LocalFree(wp);
447
+ if (failed) rb_raise(rb_eArgError, "winclip: every file path must be a String");
448
+ if (!ok) rb_raise(eWinclipError, "winclip: failed to set clipboard files");
449
+ return Qnil;
450
+ }
451
+
452
+ static VALUE wc_get_image(VALUE self) {
453
+ if (!IsClipboardFormatAvailable(CF_DIBV5) && !IsClipboardFormatAvailable(CF_DIB))
454
+ return Qnil;
455
+
456
+ HRESULT cohr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
457
+ bool com_owned = (cohr == S_OK || cohr == S_FALSE);
458
+
459
+ std::vector<BYTE> png;
460
+ HRESULT hr = E_FAIL;
461
+ bool threw = false;
462
+ try { hr = clip_get_image(png); }
463
+ catch (...) { threw = true; } /* e.g. bad_alloc in bgra_to_png (clipboard already closed) */
464
+ if (com_owned) CoUninitialize();
465
+
466
+ if (threw) rb_raise(eWinclipError, "winclip: out of memory decoding the clipboard image");
467
+ /* Format present but not decodable (palettized / malformed) -> nil, matching
468
+ * the "getters return nil when there's no usable content" contract. */
469
+ if (FAILED(hr)) return Qnil;
470
+
471
+ VALUE s = rb_str_new((char *)png.data(), (long)png.size());
472
+ rb_enc_associate(s, rb_ascii8bit_encoding());
473
+ return s;
474
+ }
475
+
476
+ static VALUE wc_set_image(VALUE self, VALUE png) {
477
+ StringValue(png);
478
+ HRESULT cohr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
479
+ bool com_owned = (cohr == S_OK || cohr == S_FALSE);
480
+
481
+ HRESULT hr = E_FAIL;
482
+ bool threw = false;
483
+ try {
484
+ std::vector<BYTE> in((BYTE *)RSTRING_PTR(png), (BYTE *)RSTRING_PTR(png) + RSTRING_LEN(png));
485
+ hr = clip_set_image(in.data(), in.size());
486
+ } catch (...) { threw = true; } /* e.g. bad_alloc decoding a huge PNG (no clipboard held) */
487
+ if (com_owned) CoUninitialize();
488
+
489
+ if (threw) rb_raise(eWinclipError, "winclip: out of memory encoding the clipboard image");
490
+ if (FAILED(hr))
491
+ rb_raise(eWinclipError, "winclip: failed to set clipboard image "
492
+ "(not a valid PNG?) (hr=0x%08lX)", (unsigned long)hr);
493
+ return Qnil;
494
+ }
495
+
496
+ static VALUE wc_clear(VALUE self) {
497
+ if (!clip_open_retry()) rb_raise(eWinclipError, "winclip: could not open the clipboard");
498
+ EmptyClipboard();
499
+ CloseClipboard();
500
+ return Qnil;
501
+ }
502
+
503
+ static VALUE wc_formats(VALUE self) {
504
+ if (!clip_open_retry()) rb_raise(eWinclipError, "winclip: could not open the clipboard");
505
+
506
+ /* Collect ids while open; build Ruby objects only after closing (so an
507
+ * allocation that raises can't leave the clipboard open). std::vector grows
508
+ * as needed — no silent cap on the number of formats reported. */
509
+ std::vector<UINT> ids;
510
+ UINT f = 0;
511
+ while ((f = EnumClipboardFormats(f)) != 0) ids.push_back(f);
512
+ CloseClipboard();
513
+
514
+ VALUE ary = rb_ary_new_capa((long)ids.size());
515
+ for (size_t i = 0; i < ids.size(); i++) {
516
+ const char *nm = clip_std_name(ids[i]);
517
+ if (nm) {
518
+ rb_ary_push(ary, rb_str_new_cstr(nm));
519
+ } else {
520
+ wchar_t wbuf[256];
521
+ int wn = GetClipboardFormatNameW(ids[i], wbuf, 256); /* no open needed */
522
+ if (wn > 0) {
523
+ char *u8 = utf16_to_utf8(wbuf, wn);
524
+ if (u8) { rb_ary_push(ary, rb_utf8_str_new_cstr(u8)); LocalFree(u8); }
525
+ }
526
+ }
527
+ }
528
+ return ary;
529
+ }
530
+
531
+ static VALUE wc_available(VALUE self, VALUE vid) {
532
+ UINT id = (UINT)NUM2UINT(vid);
533
+ return IsClipboardFormatAvailable(id) ? Qtrue : Qfalse;
534
+ }
535
+
536
+ static VALUE wc_register_format(VALUE self, VALUE name) {
537
+ VALUE u8 = rb_str_export_to_enc(StringValue(name), rb_utf8_encoding());
538
+ wchar_t *w = utf8_to_utf16(RSTRING_PTR(u8), -1);
539
+ if (!w) rb_raise(eWinclipError, "winclip: could not encode format name");
540
+ UINT id = RegisterClipboardFormatW(w);
541
+ LocalFree(w);
542
+ if (id == 0) rb_raise(eWinclipError, "winclip: could not register clipboard format");
543
+ return UINT2NUM(id);
544
+ }
545
+
546
+ static VALUE wc_has_text(VALUE self) {
547
+ return (IsClipboardFormatAvailable(CF_UNICODETEXT) ||
548
+ IsClipboardFormatAvailable(CF_TEXT)) ? Qtrue : Qfalse;
549
+ }
550
+ static VALUE wc_has_image(VALUE self) {
551
+ return (IsClipboardFormatAvailable(CF_DIBV5) ||
552
+ IsClipboardFormatAvailable(CF_DIB) ||
553
+ IsClipboardFormatAvailable(CF_BITMAP)) ? Qtrue : Qfalse;
554
+ }
555
+ static VALUE wc_has_files(VALUE self) {
556
+ return IsClipboardFormatAvailable(CF_HDROP) ? Qtrue : Qfalse;
557
+ }
558
+
559
+ extern "C" void Init_winclip(void) {
560
+ VALUE mWinclip = rb_define_module("Winclip");
561
+ eWinclipError = rb_define_class_under(mWinclip, "Error", rb_eStandardError);
562
+
563
+ rb_define_singleton_method(mWinclip, "_get_text", RUBY_METHOD_FUNC(wc_get_text), 0);
564
+ rb_define_singleton_method(mWinclip, "_set_text", RUBY_METHOD_FUNC(wc_set_text), 1);
565
+ rb_define_singleton_method(mWinclip, "_get_image", RUBY_METHOD_FUNC(wc_get_image), 0);
566
+ rb_define_singleton_method(mWinclip, "_set_image", RUBY_METHOD_FUNC(wc_set_image), 1);
567
+ rb_define_singleton_method(mWinclip, "_get_files", RUBY_METHOD_FUNC(wc_get_files), 0);
568
+ rb_define_singleton_method(mWinclip, "_set_files", RUBY_METHOD_FUNC(wc_set_files), 1);
569
+ rb_define_singleton_method(mWinclip, "clear", RUBY_METHOD_FUNC(wc_clear), 0);
570
+ rb_define_singleton_method(mWinclip, "formats", RUBY_METHOD_FUNC(wc_formats), 0);
571
+ rb_define_singleton_method(mWinclip, "_available?", RUBY_METHOD_FUNC(wc_available), 1);
572
+ rb_define_singleton_method(mWinclip, "_register_format", RUBY_METHOD_FUNC(wc_register_format), 1);
573
+ rb_define_singleton_method(mWinclip, "has_text?", RUBY_METHOD_FUNC(wc_has_text), 0);
574
+ rb_define_singleton_method(mWinclip, "has_image?", RUBY_METHOD_FUNC(wc_has_image), 0);
575
+ rb_define_singleton_method(mWinclip, "has_files?", RUBY_METHOD_FUNC(wc_has_files), 0);
576
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Winclip
4
+ VERSION = "0.1.0"
5
+ end
data/lib/winclip.rb ADDED
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "winclip/version"
4
+ require "winclip/winclip" # native extension: defines the Winclip module + primitives
5
+
6
+ # winclip — read and write the Windows clipboard: text, images (PNG), and file
7
+ # lists.
8
+ #
9
+ # Winclip.text = "hello"
10
+ # Winclip.text # => "hello"
11
+ # Winclip.image = File.binread("pic.png")
12
+ # File.binwrite("out.png", Winclip.image) if Winclip.has_image?
13
+ # Winclip.files = ["C:/a.txt", "C:/b.txt"]
14
+ # Winclip.files # => ["C:/a.txt", "C:/b.txt"]
15
+ # Winclip.clear
16
+ module Winclip
17
+ # Standard clipboard format ids, for Winclip.available?("CF_...").
18
+ STANDARD_FORMATS = {
19
+ "CF_TEXT" => 1, "CF_BITMAP" => 2, "CF_METAFILEPICT" => 3, "CF_SYLK" => 4,
20
+ "CF_DIF" => 5, "CF_TIFF" => 6, "CF_OEMTEXT" => 7, "CF_DIB" => 8,
21
+ "CF_PALETTE" => 9, "CF_PENDATA" => 10, "CF_RIFF" => 11, "CF_WAVE" => 12,
22
+ "CF_UNICODETEXT" => 13, "CF_ENHMETAFILE" => 14, "CF_HDROP" => 15,
23
+ "CF_LOCALE" => 16, "CF_DIBV5" => 17
24
+ }.freeze
25
+
26
+ class << self
27
+ # Clipboard text as a UTF-8 String, or nil if no text is present.
28
+ def text
29
+ _get_text
30
+ end
31
+
32
+ # Set the clipboard to the given text (coerced with #to_s). Returns the value.
33
+ def text=(value)
34
+ _set_text(value.to_s)
35
+ value
36
+ end
37
+
38
+ # The clipboard image as PNG bytes (binary String), or nil if no image is
39
+ # present. Pairs with windraw: `File.binwrite("x.png", Winclip.image)`.
40
+ def image
41
+ _get_image
42
+ end
43
+
44
+ # Put an image on the clipboard from PNG bytes (a binary String). Returns the
45
+ # value. Sets both CF_DIBV5 (alpha-aware) and CF_DIB for broad compatibility.
46
+ def image=(png)
47
+ _set_image(png)
48
+ png
49
+ end
50
+
51
+ # The clipboard file list as an Array<String> of paths, or nil if none.
52
+ def files
53
+ _get_files
54
+ end
55
+
56
+ # Put a file list on the clipboard (CF_HDROP). Accepts an Array of paths or a
57
+ # single path String. Returns the value.
58
+ def files=(paths)
59
+ list = Array(paths).map(&:to_s)
60
+ raise ArgumentError, "winclip: provide at least one file path" if list.empty?
61
+ _set_files(list)
62
+ paths
63
+ end
64
+
65
+ # Copy text to the clipboard (alias for text=). Returns the string.
66
+ def copy(string)
67
+ self.text = string
68
+ end
69
+
70
+ # Read text from the clipboard (alias for #text).
71
+ def paste
72
+ text
73
+ end
74
+
75
+ # Is the given clipboard format present? Accepts an Integer format id, a
76
+ # standard format name ("CF_UNICODETEXT"), a registered format name ("PNG"),
77
+ # or one of :text / :image / :files.
78
+ def available?(format)
79
+ case format
80
+ when Integer then _available?(format)
81
+ when :text then has_text?
82
+ when :image then has_image?
83
+ when :files then has_files?
84
+ else
85
+ name = format.to_s
86
+ if (id = STANDARD_FORMATS[name])
87
+ _available?(id)
88
+ else
89
+ # Resolve a custom/registered format by name WITHOUT registering it as a
90
+ # side effect (RegisterClipboardFormat would create unknown names).
91
+ # `formats` reports what's actually present — exactly the answer needed.
92
+ formats.include?(name)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: winclip
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ned
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake-compiler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.2'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: vcvars
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.1'
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 0.1.1
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '0.1'
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 0.1.1
74
+ description: |
75
+ winclip is a native Windows clipboard library for Ruby. It gets and sets
76
+ Unicode text, images (as PNG bytes, converted to/from CF_DIB/CF_DIBV5 via
77
+ WIC), and file lists (CF_HDROP), plus clear, format introspection, and
78
+ has_text?/has_image?/has_files? queries. The PNG image interface pairs
79
+ directly with the windraw gem. Windows MSVC (mswin) Ruby only.
80
+ executables: []
81
+ extensions:
82
+ - ext/winclip/extconf.rb
83
+ extra_rdoc_files: []
84
+ files:
85
+ - CHANGELOG.md
86
+ - LICENSE.txt
87
+ - README.md
88
+ - ext/winclip/extconf.rb
89
+ - ext/winclip/winclip.cpp
90
+ - lib/winclip.rb
91
+ - lib/winclip/version.rb
92
+ homepage: https://github.com/main-path/winclip
93
+ licenses:
94
+ - MIT
95
+ metadata:
96
+ homepage_uri: https://github.com/main-path/winclip
97
+ changelog_uri: https://github.com/main-path/winclip/blob/main/CHANGELOG.md
98
+ bug_tracker_uri: https://github.com/main-path/winclip/issues
99
+ rubygems_mfa_required: 'true'
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '3.0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.6.9
115
+ specification_version: 4
116
+ summary: 'Read and write the Windows clipboard: text, images (PNG), and file lists.'
117
+ test_files: []