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 +7 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/ext/winclip/extconf.rb +26 -0
- data/ext/winclip/winclip.cpp +576 -0
- data/lib/winclip/version.rb +5 -0
- data/lib/winclip.rb +97 -0
- metadata +117 -0
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
|
+
}
|
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: []
|