winclip 0.1.1 → 0.1.2
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 +4 -4
- data/README.md +11 -76
- data/lib/winclip.rb +4 -97
- metadata +12 -82
- data/CHANGELOG.md +0 -69
- data/ext/winclip/extconf.rb +0 -26
- data/ext/winclip/winclip.cpp +0 -649
- data/lib/winclip/version.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f7b8b2dc585085f55a473c7b0cc851ccdb8d4396de9390995dc27f499b547a4
|
|
4
|
+
data.tar.gz: d32841afbdd97a5725b4184c451fdb3ce0e65740713f6dda4c07dbce744adec2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4909c86dd8da6de2cdcb976e84dee6cd0ce16c1f1349d70391b5df976bfb45e08cf625d87d8d5b86e00daa3a6d50e8fb5699c2b0ac2404cd74a2308ff06dc8f
|
|
7
|
+
data.tar.gz: 0ce47536460841c38ca12f4fa789369a63dc2f517e04f556e347ca181ce0f6de32c6972c25a7a2d226775ad4ec2514d94233a2b7d9b3289671ffcea9763fdb98
|
data/README.md
CHANGED
|
@@ -1,76 +1,11 @@
|
|
|
1
|
-
# winclip
|
|
2
|
-
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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).
|
|
1
|
+
# winclip (discontinued)
|
|
2
|
+
|
|
3
|
+
**winclip is no longer maintained.** This final release (0.1.2) contains no
|
|
4
|
+
code: installing it compiles nothing, and `require "winclip"` raises a
|
|
5
|
+
`LoadError` explaining the discontinuation.
|
|
6
|
+
|
|
7
|
+
Use the clipboard gem for text; PowerShell Get-Clipboard / Set-Clipboard for images and file lists instead.
|
|
8
|
+
|
|
9
|
+
Earlier releases remain on RubyGems only because RubyGems does not allow
|
|
10
|
+
versions older than 30 days to be yanked. They are unmaintained and not
|
|
11
|
+
recommended.
|
data/lib/winclip.rb
CHANGED
|
@@ -1,97 +1,4 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
raise LoadError, "winclip is discontinued and this release contains no code. Use the clipboard gem for text; PowerShell Get-Clipboard / Set-Clipboard for images and file lists instead." \
|
|
4
|
+
" Earlier winclip releases are unmaintained and not recommended."
|
metadata
CHANGED
|
@@ -1,102 +1,31 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: winclip
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ned
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: winclip is discontinued. This final release contains no code and exists
|
|
13
|
+
only so that installing winclip explains the discontinuation instead of building
|
|
14
|
+
an unmaintained native extension.
|
|
80
15
|
executables: []
|
|
81
|
-
extensions:
|
|
82
|
-
- ext/winclip/extconf.rb
|
|
16
|
+
extensions: []
|
|
83
17
|
extra_rdoc_files: []
|
|
84
18
|
files:
|
|
85
|
-
- CHANGELOG.md
|
|
86
19
|
- LICENSE.txt
|
|
87
20
|
- README.md
|
|
88
|
-
- ext/winclip/extconf.rb
|
|
89
|
-
- ext/winclip/winclip.cpp
|
|
90
21
|
- lib/winclip.rb
|
|
91
|
-
- lib/winclip/version.rb
|
|
92
|
-
homepage: https://github.com/main-path/winclip
|
|
93
22
|
licenses:
|
|
94
23
|
- MIT
|
|
95
24
|
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
25
|
rubygems_mfa_required: 'true'
|
|
26
|
+
post_install_message: winclip is discontinued and this release contains no code. Use
|
|
27
|
+
the clipboard gem for text; PowerShell Get-Clipboard / Set-Clipboard for images
|
|
28
|
+
and file lists instead.
|
|
100
29
|
rdoc_options: []
|
|
101
30
|
require_paths:
|
|
102
31
|
- lib
|
|
@@ -104,7 +33,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
104
33
|
requirements:
|
|
105
34
|
- - ">="
|
|
106
35
|
- !ruby/object:Gem::Version
|
|
107
|
-
version: '
|
|
36
|
+
version: '2.0'
|
|
108
37
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
38
|
requirements:
|
|
110
39
|
- - ">="
|
|
@@ -113,5 +42,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
113
42
|
requirements: []
|
|
114
43
|
rubygems_version: 3.6.9
|
|
115
44
|
specification_version: 4
|
|
116
|
-
summary:
|
|
45
|
+
summary: Discontinued. Use the clipboard gem for text; PowerShell Get-Clipboard /
|
|
46
|
+
Set-Clipboard for images and file lists instead.
|
|
117
47
|
test_files: []
|
data/CHANGELOG.md
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
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.1] - 2026-06-27
|
|
10
|
-
|
|
11
|
-
### Fixed
|
|
12
|
-
- **Intermittent heap-corruption crash (silent `SIGSEGV`) when using the image
|
|
13
|
-
clipboard.** `Winclip.image` and `Winclip.image=` raised their Ruby exceptions
|
|
14
|
-
(`rb_raise`, which uses `longjmp`) from inside a function frame that also
|
|
15
|
-
carried C++ exception-handling state — a `std::vector` and a `try`/`catch`
|
|
16
|
-
around the WIC decode. On MSVC/x64 that `longjmp` unwinds through the frame and
|
|
17
|
-
corrupts the C++ unwind bookkeeping, poisoning the process heap; the corruption
|
|
18
|
-
detonated later in an unrelated allocation (often the next exception raise) as a
|
|
19
|
-
`STATUS_HEAP_CORRUPTION` with no Ruby `[BUG]` dump — so it surfaced as an
|
|
20
|
-
occasional truncated/aborted run (~2 of 3) rather than a counted test failure.
|
|
21
|
-
All C++/exception-handling work now lives in helper functions; the Ruby-facing
|
|
22
|
-
image methods are plain POD frames that `rb_raise` can unwind safely. Verified
|
|
23
|
-
with a stress harness: the previously ~98%-fatal reject path now runs 0 crashes
|
|
24
|
-
across hundreds of invocations, and the full suite is stable across randomized
|
|
25
|
-
ordering. Added `test/test_com_stability.rb` covering the reject path and image
|
|
26
|
-
round-trips.
|
|
27
|
-
|
|
28
|
-
### Changed
|
|
29
|
-
- COM/WIC is now initialized once per process instead of an apartment
|
|
30
|
-
init/teardown on every image call — robustness hardening that removes
|
|
31
|
-
needless per-call STA churn (not the cause of the crash above, but flagged
|
|
32
|
-
alongside it).
|
|
33
|
-
|
|
34
|
-
### Security / robustness
|
|
35
|
-
- Hardened untrusted-DIB pixel-offset handling in `Winclip.image`: the colour-
|
|
36
|
-
mask gap after a 40-byte `BITMAPINFOHEADER` is now also accounted for under
|
|
37
|
-
`BI_ALPHABITFIELDS` (4 masks), not only `BI_BITFIELDS` (3 masks), so pixels are
|
|
38
|
-
located correctly when the OS does not synthesize a `CF_DIBV5` (e.g. some
|
|
39
|
-
remote-desktop clipboards) and only a raw `CF_DIB` is present.
|
|
40
|
-
- The BGRA row stride (`width * 4`) handed to WIC is now computed and bounds-
|
|
41
|
-
checked in 64-bit, rejecting absurd widths instead of letting a 32-bit
|
|
42
|
-
multiply wrap. Both are defensive (bounded, never a heap over-read in prior
|
|
43
|
-
versions); they keep a malformed/hostile clipboard image decoding correctly or
|
|
44
|
-
failing cleanly rather than producing a wrong image.
|
|
45
|
-
|
|
46
|
-
## [0.1.0] - 2026-05-30
|
|
47
|
-
|
|
48
|
-
### Added
|
|
49
|
-
- `Winclip.text` / `Winclip.text=` — get/set Unicode clipboard text (UTF-8).
|
|
50
|
-
- `Winclip.image` / `Winclip.image=` — get/set the clipboard image as PNG bytes
|
|
51
|
-
(CF_DIBV5 + CF_DIB via WIC; handles 24/32bpp, top-down/bottom-up on read).
|
|
52
|
-
- `Winclip.files` / `Winclip.files=` — get/set the clipboard file list (CF_HDROP).
|
|
53
|
-
- `Winclip.copy` / `Winclip.paste` — text convenience aliases.
|
|
54
|
-
- `Winclip.clear` — empty the clipboard.
|
|
55
|
-
- `Winclip.formats` — list available clipboard format names.
|
|
56
|
-
- `Winclip.has_text?` / `has_image?` / `has_files?` and `available?(fmt)`.
|
|
57
|
-
|
|
58
|
-
### Security / robustness
|
|
59
|
-
- `Winclip.image` fully validates an untrusted clipboard DIB (minimum header
|
|
60
|
-
size, header size, and that the claimed pixels fit the actual handle) before
|
|
61
|
-
reading, and bounds allocation against the real data — preventing OOB reads
|
|
62
|
-
and runaway allocations from a malformed/hostile clipboard image. No C++
|
|
63
|
-
exception can unwind while the clipboard is held open. An undecodable image
|
|
64
|
-
returns `nil` rather than raising. Image size math is 64-bit (no overflow).
|
|
65
|
-
- `available?(name)` resolves custom format names without registering them.
|
|
66
|
-
|
|
67
|
-
[Unreleased]: https://github.com/main-path/winclip/compare/v0.1.1...HEAD
|
|
68
|
-
[0.1.1]: https://github.com/main-path/winclip/compare/v0.1.0...v0.1.1
|
|
69
|
-
[0.1.0]: https://github.com/main-path/winclip/releases/tag/v0.1.0
|
data/ext/winclip/extconf.rb
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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")
|
data/ext/winclip/winclip.cpp
DELETED
|
@@ -1,649 +0,0 @@
|
|
|
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
|
-
#ifndef BI_ALPHABITFIELDS
|
|
36
|
-
#define BI_ALPHABITFIELDS 6 /* 40-byte header + 4 colour masks (R,G,B,A) */
|
|
37
|
-
#endif
|
|
38
|
-
|
|
39
|
-
static VALUE eWinclipError; /* Winclip::Error */
|
|
40
|
-
|
|
41
|
-
/* ---------- COM lifetime -------------------------------------------------
|
|
42
|
-
* WIC runs inside an initialized COM apartment. We initialize the STA ONCE for
|
|
43
|
-
* the life of the process and never CoUninitialize between calls, instead of the
|
|
44
|
-
* old per-call CoInitializeEx/CoUninitialize pair: repeatedly creating and
|
|
45
|
-
* finalizing an apartment around a process-global WIC factory is needless churn,
|
|
46
|
-
* and a single long-lived apartment is the standard model for an in-process WIC
|
|
47
|
-
* consumer. (This is robustness hardening. The intermittent heap-corruption
|
|
48
|
-
* crash itself was a separate bug — rb_raise unwinding through a C++ EH frame —
|
|
49
|
-
* fixed via the C++/EH-confining wc_*_image helpers below, not here.) A plain
|
|
50
|
-
* static flag is safe: every Ruby-facing method runs under the GVL on the main
|
|
51
|
-
* thread. */
|
|
52
|
-
static void wc_ensure_com(void) {
|
|
53
|
-
static bool com_inited = false;
|
|
54
|
-
if (com_inited) return;
|
|
55
|
-
/* S_OK (we initialized it), S_FALSE (already STA) and RPC_E_CHANGED_MODE
|
|
56
|
-
* (thread already MTA) are all acceptable — WIC's in-proc objects work in
|
|
57
|
-
* either apartment model. We deliberately never pair this with
|
|
58
|
-
* CoUninitialize. */
|
|
59
|
-
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
|
60
|
-
com_inited = true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/* ---------- UTF-8 <-> UTF-16 (LocalAlloc'd results) ---------------------- */
|
|
64
|
-
|
|
65
|
-
static wchar_t *utf8_to_utf16(const char *utf8, int len /* -1 = NUL-terminated */) {
|
|
66
|
-
int need = MultiByteToWideChar(CP_UTF8, 0, utf8, len, NULL, 0);
|
|
67
|
-
if (need <= 0) return NULL;
|
|
68
|
-
wchar_t *w = (wchar_t *)LocalAlloc(LPTR, (size_t)need * sizeof(wchar_t));
|
|
69
|
-
if (!w) return NULL;
|
|
70
|
-
if (MultiByteToWideChar(CP_UTF8, 0, utf8, len, w, need) <= 0) { LocalFree(w); return NULL; }
|
|
71
|
-
return w;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
static char *utf16_to_utf8(const wchar_t *w, int len /* -1 = NUL-terminated */) {
|
|
75
|
-
int need = WideCharToMultiByte(CP_UTF8, 0, w, len, NULL, 0, NULL, NULL);
|
|
76
|
-
if (need <= 0) return NULL;
|
|
77
|
-
char *s = (char *)LocalAlloc(LPTR, (size_t)need);
|
|
78
|
-
if (!s) return NULL;
|
|
79
|
-
if (WideCharToMultiByte(CP_UTF8, 0, w, len, s, need, NULL, NULL) <= 0) { LocalFree(s); return NULL; }
|
|
80
|
-
return s;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* OpenClipboard with a short retry loop (another app may briefly hold it). */
|
|
84
|
-
static int clip_open_retry(void) {
|
|
85
|
-
for (int i = 0; i < 20; i++) { /* ~200 ms worst case */
|
|
86
|
-
if (OpenClipboard(NULL)) return 1;
|
|
87
|
-
Sleep(10);
|
|
88
|
-
}
|
|
89
|
-
return 0;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* ---------- text -------------------------------------------------------- */
|
|
93
|
-
|
|
94
|
-
/* 1 ok / 0 fail. */
|
|
95
|
-
static int clip_set_text(const wchar_t *w) {
|
|
96
|
-
size_t bytes = (wcslen(w) + 1) * sizeof(wchar_t); /* include NUL */
|
|
97
|
-
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
|
|
98
|
-
if (!hMem) return 0;
|
|
99
|
-
void *dst = GlobalLock(hMem);
|
|
100
|
-
if (!dst) { GlobalFree(hMem); return 0; }
|
|
101
|
-
memcpy(dst, w, bytes);
|
|
102
|
-
GlobalUnlock(hMem);
|
|
103
|
-
if (!clip_open_retry()) { GlobalFree(hMem); return 0; }
|
|
104
|
-
EmptyClipboard();
|
|
105
|
-
if (SetClipboardData(CF_UNICODETEXT, hMem) == NULL) {
|
|
106
|
-
GlobalFree(hMem); CloseClipboard(); return 0; /* failure: we own it */
|
|
107
|
-
}
|
|
108
|
-
CloseClipboard(); /* success: system owns it */
|
|
109
|
-
return 1;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/* LocalAlloc'd UTF-8 (caller LocalFree) or NULL if absent/failure. */
|
|
113
|
-
static char *clip_get_text(void) {
|
|
114
|
-
if (!IsClipboardFormatAvailable(CF_UNICODETEXT)) return NULL;
|
|
115
|
-
if (!clip_open_retry()) return NULL;
|
|
116
|
-
HANDLE h = GetClipboardData(CF_UNICODETEXT); /* clipboard-owned */
|
|
117
|
-
char *utf8 = NULL;
|
|
118
|
-
if (h) {
|
|
119
|
-
wchar_t *w = (wchar_t *)GlobalLock(h);
|
|
120
|
-
if (w) { utf8 = utf16_to_utf8(w, -1); GlobalUnlock(h); }
|
|
121
|
-
}
|
|
122
|
-
CloseClipboard();
|
|
123
|
-
return utf8;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/* ---------- files (CF_HDROP) -------------------------------------------- */
|
|
127
|
-
|
|
128
|
-
static int clip_set_files(const wchar_t **wp, int count) {
|
|
129
|
-
size_t list_cch = 1; /* trailing extra NUL */
|
|
130
|
-
for (int i = 0; i < count; i++) list_cch += wcslen(wp[i]) + 1;
|
|
131
|
-
size_t bytes = sizeof(DROPFILES) + list_cch * sizeof(wchar_t);
|
|
132
|
-
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
|
|
133
|
-
if (!hMem) return 0;
|
|
134
|
-
BYTE *base = (BYTE *)GlobalLock(hMem);
|
|
135
|
-
if (!base) { GlobalFree(hMem); return 0; }
|
|
136
|
-
DROPFILES *df = (DROPFILES *)base;
|
|
137
|
-
df->pFiles = sizeof(DROPFILES);
|
|
138
|
-
df->pt.x = 0; df->pt.y = 0;
|
|
139
|
-
df->fNC = FALSE;
|
|
140
|
-
df->fWide = TRUE;
|
|
141
|
-
wchar_t *list = (wchar_t *)(base + sizeof(DROPFILES));
|
|
142
|
-
size_t pos = 0;
|
|
143
|
-
for (int i = 0; i < count; i++) {
|
|
144
|
-
size_t n = wcslen(wp[i]) + 1;
|
|
145
|
-
memcpy(list + pos, wp[i], n * sizeof(wchar_t));
|
|
146
|
-
pos += n;
|
|
147
|
-
}
|
|
148
|
-
list[pos] = L'\0'; /* double-NUL terminate */
|
|
149
|
-
GlobalUnlock(hMem);
|
|
150
|
-
if (!clip_open_retry()) { GlobalFree(hMem); return 0; }
|
|
151
|
-
EmptyClipboard();
|
|
152
|
-
if (SetClipboardData(CF_HDROP, hMem) == NULL) { GlobalFree(hMem); CloseClipboard(); return 0; }
|
|
153
|
-
CloseClipboard();
|
|
154
|
-
return 1;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/* On success *out = LocalAlloc'd array of LocalAlloc'd UTF-8 strings; returns
|
|
158
|
-
* count (>=0). Returns -1 when no CF_HDROP / on failure. */
|
|
159
|
-
static int clip_get_files(char ***out) {
|
|
160
|
-
*out = NULL;
|
|
161
|
-
if (!IsClipboardFormatAvailable(CF_HDROP)) return -1;
|
|
162
|
-
if (!clip_open_retry()) return -1;
|
|
163
|
-
HANDLE h = GetClipboardData(CF_HDROP);
|
|
164
|
-
if (!h) { CloseClipboard(); return -1; }
|
|
165
|
-
HDROP hDrop = (HDROP)h;
|
|
166
|
-
UINT n = DragQueryFileW(hDrop, 0xFFFFFFFF, NULL, 0);
|
|
167
|
-
char **arr = (char **)LocalAlloc(LPTR, (size_t)(n ? n : 1) * sizeof(char *));
|
|
168
|
-
if (!arr) { CloseClipboard(); return -1; }
|
|
169
|
-
for (UINT i = 0; i < n; i++) {
|
|
170
|
-
UINT cch = DragQueryFileW(hDrop, i, NULL, 0);
|
|
171
|
-
wchar_t *wbuf = (wchar_t *)LocalAlloc(LPTR, (size_t)(cch + 1) * sizeof(wchar_t));
|
|
172
|
-
if (!wbuf) { for (UINT j = 0; j < i; j++) LocalFree(arr[j]); LocalFree(arr); CloseClipboard(); return -1; }
|
|
173
|
-
DragQueryFileW(hDrop, i, wbuf, cch + 1);
|
|
174
|
-
arr[i] = utf16_to_utf8(wbuf, -1);
|
|
175
|
-
LocalFree(wbuf);
|
|
176
|
-
if (!arr[i]) { for (UINT j = 0; j < i; j++) LocalFree(arr[j]); LocalFree(arr); CloseClipboard(); return -1; }
|
|
177
|
-
}
|
|
178
|
-
CloseClipboard();
|
|
179
|
-
*out = arr;
|
|
180
|
-
return (int)n;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/* ---------- image (WIC) ------------------------------------------------- */
|
|
184
|
-
|
|
185
|
-
static HRESULT png_to_bgra(const BYTE *png, size_t pnglen,
|
|
186
|
-
std::vector<BYTE> &outBgra, UINT &w, UINT &h) {
|
|
187
|
-
IStream *stream = SHCreateMemStream(png, (UINT)pnglen);
|
|
188
|
-
if (!stream) return E_OUTOFMEMORY;
|
|
189
|
-
IWICImagingFactory *factory = NULL; IWICBitmapDecoder *decoder = NULL;
|
|
190
|
-
IWICBitmapFrameDecode *frame = NULL; IWICFormatConverter *conv = NULL;
|
|
191
|
-
HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER,
|
|
192
|
-
IID_PPV_ARGS(&factory));
|
|
193
|
-
if (SUCCEEDED(hr)) hr = factory->CreateDecoderFromStream(stream, NULL, WICDecodeMetadataCacheOnDemand, &decoder);
|
|
194
|
-
if (SUCCEEDED(hr)) hr = decoder->GetFrame(0, &frame);
|
|
195
|
-
if (SUCCEEDED(hr)) hr = factory->CreateFormatConverter(&conv);
|
|
196
|
-
if (SUCCEEDED(hr)) hr = conv->Initialize(frame, GUID_WICPixelFormat32bppBGRA,
|
|
197
|
-
WICBitmapDitherTypeNone, NULL, 0.0, WICBitmapPaletteTypeCustom);
|
|
198
|
-
if (SUCCEEDED(hr)) hr = conv->GetSize(&w, &h);
|
|
199
|
-
if (SUCCEEDED(hr)) {
|
|
200
|
-
uint64_t stride64 = (uint64_t)w * 4;
|
|
201
|
-
uint64_t total = stride64 * h;
|
|
202
|
-
if (w == 0 || h == 0 || stride64 > 0xFFFFFFFFu || total > 0xFFFFFFFFu) {
|
|
203
|
-
hr = E_OUTOFMEMORY; /* WIC's CopyPixels uses UINT sizes */
|
|
204
|
-
} else {
|
|
205
|
-
UINT stride = (UINT)stride64;
|
|
206
|
-
outBgra.resize((size_t)total);
|
|
207
|
-
WICRect rc = { 0, 0, (INT)w, (INT)h };
|
|
208
|
-
hr = conv->CopyPixels(&rc, stride, (UINT)outBgra.size(), outBgra.data());
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (conv) conv->Release();
|
|
212
|
-
if (frame) frame->Release();
|
|
213
|
-
if (decoder) decoder->Release();
|
|
214
|
-
if (factory) factory->Release();
|
|
215
|
-
if (stream) stream->Release();
|
|
216
|
-
return hr;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
static HRESULT bgra_to_png(const BYTE *bgra, UINT w, UINT h, UINT stride, std::vector<BYTE> &outPng) {
|
|
220
|
-
IWICImagingFactory *factory = NULL; IWICStream *stream = NULL;
|
|
221
|
-
IWICBitmapEncoder *encoder = NULL; IWICBitmapFrameEncode *frame = NULL;
|
|
222
|
-
IStream *memstream = SHCreateMemStream(NULL, 0);
|
|
223
|
-
if (!memstream) return E_OUTOFMEMORY;
|
|
224
|
-
HRESULT hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER,
|
|
225
|
-
IID_PPV_ARGS(&factory));
|
|
226
|
-
if (SUCCEEDED(hr)) hr = factory->CreateStream(&stream);
|
|
227
|
-
if (SUCCEEDED(hr)) hr = stream->InitializeFromIStream(memstream);
|
|
228
|
-
if (SUCCEEDED(hr)) hr = factory->CreateEncoder(GUID_ContainerFormatPng, NULL, &encoder);
|
|
229
|
-
if (SUCCEEDED(hr)) hr = encoder->Initialize(stream, WICBitmapEncoderNoCache);
|
|
230
|
-
if (SUCCEEDED(hr)) hr = encoder->CreateNewFrame(&frame, NULL);
|
|
231
|
-
if (SUCCEEDED(hr)) hr = frame->Initialize(NULL);
|
|
232
|
-
if (SUCCEEDED(hr)) hr = frame->SetSize(w, h);
|
|
233
|
-
WICPixelFormatGUID fmt = GUID_WICPixelFormat32bppBGRA;
|
|
234
|
-
if (SUCCEEDED(hr)) hr = frame->SetPixelFormat(&fmt);
|
|
235
|
-
if (SUCCEEDED(hr)) {
|
|
236
|
-
uint64_t cb = (uint64_t)stride * h;
|
|
237
|
-
if (cb > 0xFFFFFFFFu) hr = E_OUTOFMEMORY;
|
|
238
|
-
else hr = frame->WritePixels(h, stride, (UINT)cb, (BYTE *)bgra);
|
|
239
|
-
}
|
|
240
|
-
if (SUCCEEDED(hr)) hr = frame->Commit();
|
|
241
|
-
if (SUCCEEDED(hr)) hr = encoder->Commit();
|
|
242
|
-
if (SUCCEEDED(hr)) {
|
|
243
|
-
STATSTG st; memset(&st, 0, sizeof(st));
|
|
244
|
-
hr = memstream->Stat(&st, STATFLAG_NONAME);
|
|
245
|
-
if (SUCCEEDED(hr)) {
|
|
246
|
-
ULONG size = (ULONG)st.cbSize.QuadPart;
|
|
247
|
-
outPng.resize(size);
|
|
248
|
-
LARGE_INTEGER zero; zero.QuadPart = 0;
|
|
249
|
-
memstream->Seek(zero, STREAM_SEEK_SET, NULL);
|
|
250
|
-
ULONG read = 0;
|
|
251
|
-
if (size) hr = memstream->Read(outPng.data(), size, &read);
|
|
252
|
-
if (SUCCEEDED(hr)) outPng.resize(read);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (frame) frame->Release();
|
|
256
|
-
if (encoder) encoder->Release();
|
|
257
|
-
if (stream) stream->Release();
|
|
258
|
-
if (factory) factory->Release();
|
|
259
|
-
if (memstream) memstream->Release();
|
|
260
|
-
return hr;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/* PNG bytes -> clipboard (sets CF_DIBV5 + CF_DIB). Caller must have COM init'd. */
|
|
264
|
-
static HRESULT clip_set_image(const BYTE *png, size_t pnglen) {
|
|
265
|
-
std::vector<BYTE> bgra; UINT w = 0, h = 0;
|
|
266
|
-
HRESULT hr = png_to_bgra(png, pnglen, bgra, w, h);
|
|
267
|
-
if (FAILED(hr)) return hr;
|
|
268
|
-
|
|
269
|
-
/* Compute the DIB byte size in 64-bit and reject images too large to encode
|
|
270
|
-
* as a clipboard DIB (avoids the w*4*h 32-bit wrap that would corrupt it). */
|
|
271
|
-
uint64_t imgSize64 = (uint64_t)w * 4 * h;
|
|
272
|
-
if (imgSize64 == 0 || imgSize64 + sizeof(BITMAPV5HEADER) > 0xFFFFFFFFu)
|
|
273
|
-
return E_OUTOFMEMORY;
|
|
274
|
-
const DWORD imgSize = (DWORD)imgSize64;
|
|
275
|
-
|
|
276
|
-
HGLOBAL hV5 = GlobalAlloc(GMEM_MOVEABLE, sizeof(BITMAPV5HEADER) + imgSize);
|
|
277
|
-
if (!hV5) return E_OUTOFMEMORY;
|
|
278
|
-
{
|
|
279
|
-
BYTE *p = (BYTE *)GlobalLock(hV5);
|
|
280
|
-
if (!p) { GlobalFree(hV5); return E_OUTOFMEMORY; }
|
|
281
|
-
BITMAPV5HEADER *bi = (BITMAPV5HEADER *)p; memset(bi, 0, sizeof(*bi));
|
|
282
|
-
bi->bV5Size = sizeof(BITMAPV5HEADER);
|
|
283
|
-
bi->bV5Width = (LONG)w; bi->bV5Height = -(LONG)h; /* top-down */
|
|
284
|
-
bi->bV5Planes = 1; bi->bV5BitCount = 32;
|
|
285
|
-
bi->bV5Compression = BI_BITFIELDS; bi->bV5SizeImage = imgSize;
|
|
286
|
-
bi->bV5RedMask = 0x00FF0000; bi->bV5GreenMask = 0x0000FF00;
|
|
287
|
-
bi->bV5BlueMask = 0x000000FF; bi->bV5AlphaMask = 0xFF000000;
|
|
288
|
-
bi->bV5CSType = LCS_sRGB; bi->bV5Intent = LCS_GM_IMAGES;
|
|
289
|
-
memcpy(p + sizeof(BITMAPV5HEADER), bgra.data(), imgSize);
|
|
290
|
-
GlobalUnlock(hV5);
|
|
291
|
-
}
|
|
292
|
-
HGLOBAL hDib = GlobalAlloc(GMEM_MOVEABLE, sizeof(BITMAPINFOHEADER) + imgSize);
|
|
293
|
-
if (!hDib) { GlobalFree(hV5); return E_OUTOFMEMORY; }
|
|
294
|
-
{
|
|
295
|
-
BYTE *p = (BYTE *)GlobalLock(hDib);
|
|
296
|
-
if (!p) { GlobalFree(hV5); GlobalFree(hDib); return E_OUTOFMEMORY; }
|
|
297
|
-
BITMAPINFOHEADER *bi = (BITMAPINFOHEADER *)p; memset(bi, 0, sizeof(*bi));
|
|
298
|
-
bi->biSize = sizeof(BITMAPINFOHEADER);
|
|
299
|
-
bi->biWidth = (LONG)w; bi->biHeight = -(LONG)h; /* top-down */
|
|
300
|
-
bi->biPlanes = 1; bi->biBitCount = 32;
|
|
301
|
-
bi->biCompression = BI_RGB; bi->biSizeImage = imgSize;
|
|
302
|
-
memcpy(p + sizeof(BITMAPINFOHEADER), bgra.data(), imgSize);
|
|
303
|
-
GlobalUnlock(hDib);
|
|
304
|
-
}
|
|
305
|
-
if (!clip_open_retry()) { GlobalFree(hV5); GlobalFree(hDib); return E_FAIL; }
|
|
306
|
-
EmptyClipboard();
|
|
307
|
-
bool ok = true;
|
|
308
|
-
if (!SetClipboardData(CF_DIBV5, hV5)) { ok = false; GlobalFree(hV5); }
|
|
309
|
-
if (!SetClipboardData(CF_DIB, hDib)) { ok = false; GlobalFree(hDib); }
|
|
310
|
-
CloseClipboard();
|
|
311
|
-
return ok ? S_OK : E_FAIL;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/* clipboard image -> PNG bytes. Caller must have COM init'd.
|
|
315
|
-
*
|
|
316
|
-
* The DIB is UNTRUSTED data placed by another app, so every field and offset is
|
|
317
|
-
* validated against the real handle size before use, and the pixel-copy region
|
|
318
|
-
* runs inside a try/catch so a std::bad_alloc can never unwind out while the
|
|
319
|
-
* clipboard is held open (which would wedge the clipboard system-wide).
|
|
320
|
-
* Returns E_FAIL for absent / malformed / unsupported (e.g. palettized) DIBs. */
|
|
321
|
-
static HRESULT clip_get_image(std::vector<BYTE> &outPng) {
|
|
322
|
-
if (!clip_open_retry()) return E_FAIL;
|
|
323
|
-
HANDLE h = GetClipboardData(CF_DIBV5);
|
|
324
|
-
if (!h) h = GetClipboardData(CF_DIB);
|
|
325
|
-
if (!h) { CloseClipboard(); return E_FAIL; }
|
|
326
|
-
BYTE *dib = (BYTE *)GlobalLock(h);
|
|
327
|
-
if (!dib) { CloseClipboard(); return E_FAIL; }
|
|
328
|
-
SIZE_T dibSize = GlobalSize(h);
|
|
329
|
-
|
|
330
|
-
HRESULT hr = S_OK;
|
|
331
|
-
std::vector<BYTE> bgra;
|
|
332
|
-
UINT w = 0, hh = 0;
|
|
333
|
-
try {
|
|
334
|
-
/* The block must be at least a BITMAPINFOHEADER before we read fields. */
|
|
335
|
-
if (dibSize < sizeof(BITMAPINFOHEADER)) throw E_FAIL;
|
|
336
|
-
|
|
337
|
-
BITMAPINFOHEADER *bih = (BITMAPINFOHEADER *)dib;
|
|
338
|
-
DWORD biSize = bih->biSize, comp = bih->biCompression;
|
|
339
|
-
LONG rawH = bih->biHeight;
|
|
340
|
-
WORD bpp = bih->biBitCount;
|
|
341
|
-
bool topDown = (rawH < 0);
|
|
342
|
-
w = (UINT)(bih->biWidth < 0 ? -bih->biWidth : bih->biWidth);
|
|
343
|
-
hh = (UINT)(rawH < 0 ? -rawH : rawH);
|
|
344
|
-
if (w == 0 || hh == 0) throw E_FAIL;
|
|
345
|
-
if (biSize < sizeof(BITMAPINFOHEADER) || biSize > dibSize) throw E_FAIL;
|
|
346
|
-
/* The BGRA stride (w*4) is handed to WIC's WritePixels as a UINT and is
|
|
347
|
-
* used as a row stride below; reject widths whose stride would not fit a
|
|
348
|
-
* 32-bit UINT so the `w * 4` expressions can never wrap. */
|
|
349
|
-
if ((uint64_t)w * 4 > 0xFFFFFFFFu) throw E_FAIL;
|
|
350
|
-
|
|
351
|
-
/* Locate the pixel array. For a plain 40-byte BITMAPINFOHEADER the colour
|
|
352
|
-
* masks sit between the header and the pixels: 3 DWORDs for BI_BITFIELDS,
|
|
353
|
-
* 4 for BI_ALPHABITFIELDS. V4/V5 headers (biSize >= 108) embed their masks,
|
|
354
|
-
* so no gap is added for them. */
|
|
355
|
-
SIZE_T pixOffset = biSize;
|
|
356
|
-
if (biSize == sizeof(BITMAPINFOHEADER)) {
|
|
357
|
-
if (comp == BI_BITFIELDS) pixOffset += 3 * sizeof(DWORD);
|
|
358
|
-
else if (comp == BI_ALPHABITFIELDS) pixOffset += 4 * sizeof(DWORD);
|
|
359
|
-
}
|
|
360
|
-
if (pixOffset > dibSize) throw E_FAIL;
|
|
361
|
-
BYTE *pixels = dib + pixOffset;
|
|
362
|
-
SIZE_T pixBytes = dibSize - pixOffset;
|
|
363
|
-
|
|
364
|
-
/* Bound the source against the bytes actually present BEFORE allocating,
|
|
365
|
-
* so untrusted dimensions can't drive a huge allocation. */
|
|
366
|
-
uint64_t srcStride = (bpp == 32) ? (uint64_t)w * 4
|
|
367
|
-
: (bpp == 24) ? (((uint64_t)w * 3 + 3) & ~3ull)
|
|
368
|
-
: 0;
|
|
369
|
-
if (srcStride == 0) throw E_FAIL; /* unsupported bpp */
|
|
370
|
-
if (srcStride * hh > (uint64_t)pixBytes) throw E_FAIL; /* claims more than it carries */
|
|
371
|
-
|
|
372
|
-
bgra.resize((size_t)w * hh * 4);
|
|
373
|
-
DWORD ss = (DWORD)srcStride;
|
|
374
|
-
if (bpp == 32) {
|
|
375
|
-
for (UINT y = 0; y < hh; y++) {
|
|
376
|
-
UINT sr = topDown ? y : (hh - 1 - y);
|
|
377
|
-
memcpy(&bgra[(size_t)y * w * 4], pixels + (size_t)sr * ss, (size_t)w * 4);
|
|
378
|
-
}
|
|
379
|
-
} else { /* bpp == 24 */
|
|
380
|
-
for (UINT y = 0; y < hh; y++) {
|
|
381
|
-
UINT sr = topDown ? y : (hh - 1 - y);
|
|
382
|
-
BYTE *srow = pixels + (size_t)sr * ss;
|
|
383
|
-
BYTE *drow = &bgra[(size_t)y * w * 4];
|
|
384
|
-
for (UINT x = 0; x < w; x++) {
|
|
385
|
-
drow[x * 4 + 0] = srow[x * 3 + 0];
|
|
386
|
-
drow[x * 4 + 1] = srow[x * 3 + 1];
|
|
387
|
-
drow[x * 4 + 2] = srow[x * 3 + 2];
|
|
388
|
-
drow[x * 4 + 3] = 255;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
} catch (HRESULT e) {
|
|
393
|
-
hr = e;
|
|
394
|
-
} catch (...) {
|
|
395
|
-
hr = E_OUTOFMEMORY; /* e.g. std::bad_alloc — never escape with clipboard open */
|
|
396
|
-
}
|
|
397
|
-
GlobalUnlock(h);
|
|
398
|
-
CloseClipboard();
|
|
399
|
-
if (FAILED(hr)) return hr;
|
|
400
|
-
return bgra_to_png(bgra.data(), w, hh, (UINT)((uint64_t)w * 4), outPng);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/* ---------- image: C++/EH-confining wrappers ----------------------------
|
|
404
|
-
* The image paths use std::vector and try/catch, which make their enclosing
|
|
405
|
-
* function carry MSVC C++ exception-handling unwind state. rb_raise unwinds via
|
|
406
|
-
* setjmp/longjmp, and on x64 MSVC that longjmp runs frame cleanup as it goes;
|
|
407
|
-
* letting it unwind THROUGH a frame that owns C++ EH state corrupts the unwind
|
|
408
|
-
* bookkeeping and poisons the heap (a later allocation then dies with a silent
|
|
409
|
-
* STATUS_HEAP_CORRUPTION). So all C++ work is confined to these helpers, which
|
|
410
|
-
* NEVER call into Ruby; the Ruby-facing wc_*_image frames below stay pure POD
|
|
411
|
-
* (no destructors, no try/catch) and are the only ones that rb_raise. */
|
|
412
|
-
|
|
413
|
-
/* 0 = ok (result in *hr), 1 = C++ exception (e.g. bad_alloc). Never raises. */
|
|
414
|
-
static int set_image_guarded(const BYTE *png, size_t len, HRESULT *hr) {
|
|
415
|
-
try {
|
|
416
|
-
std::vector<BYTE> in(png, png + len);
|
|
417
|
-
*hr = clip_set_image(in.data(), in.size());
|
|
418
|
-
} catch (...) {
|
|
419
|
-
return 1;
|
|
420
|
-
}
|
|
421
|
-
return 0;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/* 0 = ok (*out is a LocalAlloc'd PNG buffer of *outlen bytes, caller LocalFree's),
|
|
425
|
-
* 1 = C++ exception, 2 = absent/undecodable. Never raises. */
|
|
426
|
-
static int get_image_guarded(BYTE **out, size_t *outlen, HRESULT *hr) {
|
|
427
|
-
*out = NULL; *outlen = 0;
|
|
428
|
-
std::vector<BYTE> png;
|
|
429
|
-
try {
|
|
430
|
-
*hr = clip_get_image(png);
|
|
431
|
-
} catch (...) {
|
|
432
|
-
return 1;
|
|
433
|
-
}
|
|
434
|
-
if (FAILED(*hr)) return 2;
|
|
435
|
-
size_t n = png.size();
|
|
436
|
-
BYTE *buf = (BYTE *)LocalAlloc(LMEM_FIXED, n ? n : 1);
|
|
437
|
-
if (!buf) return 1;
|
|
438
|
-
if (n) memcpy(buf, png.data(), n);
|
|
439
|
-
*out = buf; *outlen = n;
|
|
440
|
-
return 0;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/* ---------- format names ------------------------------------------------ */
|
|
444
|
-
|
|
445
|
-
static const char *clip_std_name(UINT f) {
|
|
446
|
-
switch (f) {
|
|
447
|
-
case CF_TEXT: return "CF_TEXT";
|
|
448
|
-
case CF_BITMAP: return "CF_BITMAP";
|
|
449
|
-
case CF_METAFILEPICT: return "CF_METAFILEPICT";
|
|
450
|
-
case CF_SYLK: return "CF_SYLK";
|
|
451
|
-
case CF_DIF: return "CF_DIF";
|
|
452
|
-
case CF_TIFF: return "CF_TIFF";
|
|
453
|
-
case CF_OEMTEXT: return "CF_OEMTEXT";
|
|
454
|
-
case CF_DIB: return "CF_DIB";
|
|
455
|
-
case CF_PALETTE: return "CF_PALETTE";
|
|
456
|
-
case CF_PENDATA: return "CF_PENDATA";
|
|
457
|
-
case CF_RIFF: return "CF_RIFF";
|
|
458
|
-
case CF_WAVE: return "CF_WAVE";
|
|
459
|
-
case CF_UNICODETEXT: return "CF_UNICODETEXT";
|
|
460
|
-
case CF_ENHMETAFILE: return "CF_ENHMETAFILE";
|
|
461
|
-
case CF_HDROP: return "CF_HDROP";
|
|
462
|
-
case CF_LOCALE: return "CF_LOCALE";
|
|
463
|
-
case CF_DIBV5: return "CF_DIBV5";
|
|
464
|
-
default: return NULL;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/* ====================== Ruby-facing methods ============================= */
|
|
469
|
-
|
|
470
|
-
static VALUE wc_get_text(VALUE self) {
|
|
471
|
-
char *u8 = clip_get_text();
|
|
472
|
-
if (!u8) return Qnil;
|
|
473
|
-
VALUE s = rb_utf8_str_new_cstr(u8);
|
|
474
|
-
LocalFree(u8);
|
|
475
|
-
return s;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
static VALUE wc_set_text(VALUE self, VALUE str) {
|
|
479
|
-
VALUE u8 = rb_str_export_to_enc(StringValue(str), rb_utf8_encoding());
|
|
480
|
-
wchar_t *w = utf8_to_utf16(RSTRING_PTR(u8), -1);
|
|
481
|
-
if (!w) rb_raise(eWinclipError, "winclip: could not encode text");
|
|
482
|
-
int ok = clip_set_text(w);
|
|
483
|
-
LocalFree(w);
|
|
484
|
-
if (!ok) rb_raise(eWinclipError, "winclip: failed to set clipboard text");
|
|
485
|
-
return Qnil;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
static VALUE wc_get_files(VALUE self) {
|
|
489
|
-
char **arr = NULL;
|
|
490
|
-
int n = clip_get_files(&arr);
|
|
491
|
-
if (n < 0) return Qnil;
|
|
492
|
-
VALUE rary = rb_ary_new_capa(n);
|
|
493
|
-
for (int i = 0; i < n; i++) {
|
|
494
|
-
rb_ary_push(rary, rb_utf8_str_new_cstr(arr[i]));
|
|
495
|
-
LocalFree(arr[i]);
|
|
496
|
-
}
|
|
497
|
-
LocalFree(arr);
|
|
498
|
-
return rary;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
static VALUE wc_set_files(VALUE self, VALUE ary) {
|
|
502
|
-
Check_Type(ary, T_ARRAY);
|
|
503
|
-
long n = RARRAY_LEN(ary);
|
|
504
|
-
if (n <= 0) rb_raise(rb_eArgError, "winclip: files list must not be empty");
|
|
505
|
-
|
|
506
|
-
/* Convert all paths to UTF-16 up front (raises here, before any clipboard
|
|
507
|
-
* work or HGLOBAL exists). */
|
|
508
|
-
wchar_t **wp = (wchar_t **)LocalAlloc(LPTR, (size_t)n * sizeof(wchar_t *));
|
|
509
|
-
if (!wp) rb_raise(rb_eNoMemError, "winclip: out of memory");
|
|
510
|
-
int built = 0;
|
|
511
|
-
int failed = 0;
|
|
512
|
-
for (long i = 0; i < n; i++) {
|
|
513
|
-
VALUE e = rb_ary_entry(ary, i);
|
|
514
|
-
if (!RB_TYPE_P(e, T_STRING)) { failed = 1; break; }
|
|
515
|
-
VALUE u8 = rb_str_export_to_enc(e, rb_utf8_encoding());
|
|
516
|
-
wp[i] = utf8_to_utf16(RSTRING_PTR(u8), -1);
|
|
517
|
-
if (!wp[i]) { failed = 1; break; }
|
|
518
|
-
built++;
|
|
519
|
-
}
|
|
520
|
-
int ok = 0;
|
|
521
|
-
if (!failed) ok = clip_set_files((const wchar_t **)wp, (int)n);
|
|
522
|
-
for (int i = 0; i < built; i++) LocalFree(wp[i]);
|
|
523
|
-
LocalFree(wp);
|
|
524
|
-
if (failed) rb_raise(rb_eArgError, "winclip: every file path must be a String");
|
|
525
|
-
if (!ok) rb_raise(eWinclipError, "winclip: failed to set clipboard files");
|
|
526
|
-
return Qnil;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
static VALUE wc_get_image(VALUE self) {
|
|
530
|
-
if (!IsClipboardFormatAvailable(CF_DIBV5) && !IsClipboardFormatAvailable(CF_DIB))
|
|
531
|
-
return Qnil;
|
|
532
|
-
|
|
533
|
-
wc_ensure_com(); /* one process-lifetime STA; never CoUninitialize'd */
|
|
534
|
-
|
|
535
|
-
/* Pure-POD frame: all C++/EH is inside get_image_guarded, so the rb_raise
|
|
536
|
-
* below never unwinds through a frame carrying C++ exception state. */
|
|
537
|
-
BYTE *buf = NULL; size_t buflen = 0; HRESULT hr = E_FAIL;
|
|
538
|
-
int rc = get_image_guarded(&buf, &buflen, &hr);
|
|
539
|
-
|
|
540
|
-
if (rc == 1) rb_raise(eWinclipError, "winclip: out of memory decoding the clipboard image");
|
|
541
|
-
/* Format present but not decodable (palettized / malformed) -> nil, matching
|
|
542
|
-
* the "getters return nil when there's no usable content" contract. */
|
|
543
|
-
if (rc == 2) return Qnil;
|
|
544
|
-
|
|
545
|
-
VALUE s = rb_str_new((char *)buf, (long)buflen);
|
|
546
|
-
LocalFree(buf);
|
|
547
|
-
rb_enc_associate(s, rb_ascii8bit_encoding());
|
|
548
|
-
return s;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
static VALUE wc_set_image(VALUE self, VALUE png) {
|
|
552
|
-
StringValue(png);
|
|
553
|
-
wc_ensure_com(); /* one process-lifetime STA; never CoUninitialize'd */
|
|
554
|
-
|
|
555
|
-
/* Pure-POD frame: the byte-vector and try/catch live in set_image_guarded so
|
|
556
|
-
* the rb_raise calls below never unwind through a C++ EH frame. */
|
|
557
|
-
const BYTE *p = (const BYTE *)RSTRING_PTR(png);
|
|
558
|
-
size_t n = (size_t)RSTRING_LEN(png);
|
|
559
|
-
HRESULT hr = E_FAIL;
|
|
560
|
-
int threw = set_image_guarded(p, n, &hr);
|
|
561
|
-
|
|
562
|
-
if (threw) rb_raise(eWinclipError, "winclip: out of memory encoding the clipboard image");
|
|
563
|
-
if (FAILED(hr))
|
|
564
|
-
rb_raise(eWinclipError, "winclip: failed to set clipboard image "
|
|
565
|
-
"(not a valid PNG?) (hr=0x%08lX)", (unsigned long)hr);
|
|
566
|
-
return Qnil;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
static VALUE wc_clear(VALUE self) {
|
|
570
|
-
if (!clip_open_retry()) rb_raise(eWinclipError, "winclip: could not open the clipboard");
|
|
571
|
-
EmptyClipboard();
|
|
572
|
-
CloseClipboard();
|
|
573
|
-
return Qnil;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
static VALUE wc_formats(VALUE self) {
|
|
577
|
-
if (!clip_open_retry()) rb_raise(eWinclipError, "winclip: could not open the clipboard");
|
|
578
|
-
|
|
579
|
-
/* Collect ids while open; build Ruby objects only after closing (so an
|
|
580
|
-
* allocation that raises can't leave the clipboard open). std::vector grows
|
|
581
|
-
* as needed — no silent cap on the number of formats reported. */
|
|
582
|
-
std::vector<UINT> ids;
|
|
583
|
-
UINT f = 0;
|
|
584
|
-
while ((f = EnumClipboardFormats(f)) != 0) ids.push_back(f);
|
|
585
|
-
CloseClipboard();
|
|
586
|
-
|
|
587
|
-
VALUE ary = rb_ary_new_capa((long)ids.size());
|
|
588
|
-
for (size_t i = 0; i < ids.size(); i++) {
|
|
589
|
-
const char *nm = clip_std_name(ids[i]);
|
|
590
|
-
if (nm) {
|
|
591
|
-
rb_ary_push(ary, rb_str_new_cstr(nm));
|
|
592
|
-
} else {
|
|
593
|
-
wchar_t wbuf[256];
|
|
594
|
-
int wn = GetClipboardFormatNameW(ids[i], wbuf, 256); /* no open needed */
|
|
595
|
-
if (wn > 0) {
|
|
596
|
-
char *u8 = utf16_to_utf8(wbuf, wn);
|
|
597
|
-
if (u8) { rb_ary_push(ary, rb_utf8_str_new_cstr(u8)); LocalFree(u8); }
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return ary;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
static VALUE wc_available(VALUE self, VALUE vid) {
|
|
605
|
-
UINT id = (UINT)NUM2UINT(vid);
|
|
606
|
-
return IsClipboardFormatAvailable(id) ? Qtrue : Qfalse;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
static VALUE wc_register_format(VALUE self, VALUE name) {
|
|
610
|
-
VALUE u8 = rb_str_export_to_enc(StringValue(name), rb_utf8_encoding());
|
|
611
|
-
wchar_t *w = utf8_to_utf16(RSTRING_PTR(u8), -1);
|
|
612
|
-
if (!w) rb_raise(eWinclipError, "winclip: could not encode format name");
|
|
613
|
-
UINT id = RegisterClipboardFormatW(w);
|
|
614
|
-
LocalFree(w);
|
|
615
|
-
if (id == 0) rb_raise(eWinclipError, "winclip: could not register clipboard format");
|
|
616
|
-
return UINT2NUM(id);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
static VALUE wc_has_text(VALUE self) {
|
|
620
|
-
return (IsClipboardFormatAvailable(CF_UNICODETEXT) ||
|
|
621
|
-
IsClipboardFormatAvailable(CF_TEXT)) ? Qtrue : Qfalse;
|
|
622
|
-
}
|
|
623
|
-
static VALUE wc_has_image(VALUE self) {
|
|
624
|
-
return (IsClipboardFormatAvailable(CF_DIBV5) ||
|
|
625
|
-
IsClipboardFormatAvailable(CF_DIB) ||
|
|
626
|
-
IsClipboardFormatAvailable(CF_BITMAP)) ? Qtrue : Qfalse;
|
|
627
|
-
}
|
|
628
|
-
static VALUE wc_has_files(VALUE self) {
|
|
629
|
-
return IsClipboardFormatAvailable(CF_HDROP) ? Qtrue : Qfalse;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
extern "C" void Init_winclip(void) {
|
|
633
|
-
VALUE mWinclip = rb_define_module("Winclip");
|
|
634
|
-
eWinclipError = rb_define_class_under(mWinclip, "Error", rb_eStandardError);
|
|
635
|
-
|
|
636
|
-
rb_define_singleton_method(mWinclip, "_get_text", RUBY_METHOD_FUNC(wc_get_text), 0);
|
|
637
|
-
rb_define_singleton_method(mWinclip, "_set_text", RUBY_METHOD_FUNC(wc_set_text), 1);
|
|
638
|
-
rb_define_singleton_method(mWinclip, "_get_image", RUBY_METHOD_FUNC(wc_get_image), 0);
|
|
639
|
-
rb_define_singleton_method(mWinclip, "_set_image", RUBY_METHOD_FUNC(wc_set_image), 1);
|
|
640
|
-
rb_define_singleton_method(mWinclip, "_get_files", RUBY_METHOD_FUNC(wc_get_files), 0);
|
|
641
|
-
rb_define_singleton_method(mWinclip, "_set_files", RUBY_METHOD_FUNC(wc_set_files), 1);
|
|
642
|
-
rb_define_singleton_method(mWinclip, "clear", RUBY_METHOD_FUNC(wc_clear), 0);
|
|
643
|
-
rb_define_singleton_method(mWinclip, "formats", RUBY_METHOD_FUNC(wc_formats), 0);
|
|
644
|
-
rb_define_singleton_method(mWinclip, "_available?", RUBY_METHOD_FUNC(wc_available), 1);
|
|
645
|
-
rb_define_singleton_method(mWinclip, "_register_format", RUBY_METHOD_FUNC(wc_register_format), 1);
|
|
646
|
-
rb_define_singleton_method(mWinclip, "has_text?", RUBY_METHOD_FUNC(wc_has_text), 0);
|
|
647
|
-
rb_define_singleton_method(mWinclip, "has_image?", RUBY_METHOD_FUNC(wc_has_image), 0);
|
|
648
|
-
rb_define_singleton_method(mWinclip, "has_files?", RUBY_METHOD_FUNC(wc_has_files), 0);
|
|
649
|
-
}
|