win32screenshot 0.0.8 → 1.0.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.
- data/.gitignore +3 -1
- data/{spec/spec.opts → .rspec} +0 -0
- data/.yardopts +6 -0
- data/History.rdoc +7 -0
- data/README.rdoc +22 -76
- data/Rakefile +8 -25
- data/VERSION +1 -1
- data/ext/CORE_RL_bzlib_.dll +0 -0
- data/ext/CORE_RL_jpeg_.dll +0 -0
- data/ext/CORE_RL_lcms_.dll +0 -0
- data/ext/CORE_RL_magick_.dll +0 -0
- data/ext/CORE_RL_png_.dll +0 -0
- data/ext/CORE_RL_ttf_.dll +0 -0
- data/ext/CORE_RL_wand_.dll +0 -0
- data/ext/CORE_RL_zlib_.dll +0 -0
- data/ext/ImageMagick-License.txt +101 -0
- data/ext/X11.dll +0 -0
- data/ext/identify.exe +0 -0
- data/ext/modules/coders/IM_MOD_RL_bmp_.dll +0 -0
- data/ext/modules/coders/IM_MOD_RL_gif_.dll +0 -0
- data/ext/modules/coders/IM_MOD_RL_jpeg_.dll +0 -0
- data/ext/modules/coders/IM_MOD_RL_png_.dll +0 -0
- data/ext/mogrify.exe +0 -0
- data/ext/msvcr100.dll +0 -0
- data/ext/vcomp100.dll +0 -0
- data/lib/win32/screenshot.rb +12 -93
- data/lib/win32/screenshot/bitmap_maker.rb +17 -119
- data/lib/win32/screenshot/extensions/rautomation/adapter/ffi/functions.rb +12 -0
- data/lib/win32/screenshot/extensions/rautomation/adapter/ffi/window.rb +17 -0
- data/lib/win32/screenshot/image.rb +43 -0
- data/lib/win32/screenshot/take.rb +102 -0
- data/spec/spec_helper.rb +13 -50
- data/spec/win32/screenshot/image_spec.rb +68 -0
- data/spec/win32/screenshot/take_spec.rb +137 -0
- data/win32screenshot.gemspec +39 -17
- metadata +50 -27
- data/lib/win32/util.rb +0 -93
- data/spec/win32_screenshot_spec.rb +0 -194
- data/spec/win32_screenshot_util_spec.rb +0 -75
@@ -1,53 +1,25 @@
|
|
1
|
-
require 'ffi'
|
2
|
-
|
3
1
|
module Win32
|
4
|
-
|
5
|
-
#
|
6
|
-
class
|
2
|
+
module Screenshot
|
3
|
+
# @private
|
4
|
+
# This is an internal class for taking the actual screenshots and not part of a public API.
|
5
|
+
class BitmapMaker
|
7
6
|
class << self
|
8
7
|
extend FFI::Library
|
9
8
|
|
10
9
|
ffi_lib 'user32', 'gdi32'
|
11
10
|
ffi_convention :stdcall
|
12
|
-
callback :enum_callback, [:long, :pointer], :bool
|
13
11
|
|
14
12
|
# user32.dll
|
15
|
-
attach_function :enum_windows, :EnumWindows,
|
16
|
-
[:enum_callback, :pointer], :int
|
17
|
-
attach_function :enum_child_windows, :EnumChildWindows,
|
18
|
-
[:long, :enum_callback, :pointer], :int
|
19
|
-
attach_function :window_text, :GetWindowTextA,
|
20
|
-
[:long, :pointer, :int], :int
|
21
|
-
attach_function :window_text_length, :GetWindowTextLengthA,
|
22
|
-
[:long], :int
|
23
|
-
attach_function :class_name, :GetClassNameA,
|
24
|
-
[:long, :pointer, :int], :int
|
25
|
-
attach_function :window_visible, :IsWindowVisible,
|
26
|
-
[:long], :bool
|
27
13
|
attach_function :dc, :GetDC,
|
28
14
|
[:long], :long
|
29
15
|
attach_function :client_rect, :GetClientRect,
|
30
16
|
[:long, :pointer], :bool
|
31
17
|
attach_function :window_rect, :GetWindowRect,
|
32
18
|
[:long, :pointer], :bool
|
33
|
-
attach_function :minimized, :IsIconic,
|
34
|
-
[:long], :bool
|
35
|
-
attach_function :show_window, :ShowWindow,
|
36
|
-
[:long, :int], :bool
|
37
19
|
attach_function :foreground_window, :GetForegroundWindow,
|
38
20
|
[], :long
|
39
21
|
attach_function :desktop_window, :GetDesktopWindow,
|
40
22
|
[], :long
|
41
|
-
attach_function :window_thread_process_id, :GetWindowThreadProcessId,
|
42
|
-
[:long, :pointer], :long
|
43
|
-
attach_function :attach_thread_input, :AttachThreadInput,
|
44
|
-
[:long, :long, :bool], :bool
|
45
|
-
attach_function :set_foreground_window, :SetForegroundWindow,
|
46
|
-
[:long], :bool
|
47
|
-
attach_function :bring_window_to_top, :BringWindowToTop,
|
48
|
-
[:long], :bool
|
49
|
-
attach_function :set_active_window, :SetActiveWindow,
|
50
|
-
[:long], :long
|
51
23
|
|
52
24
|
# gdi32.dll
|
53
25
|
attach_function :create_compatible_dc, :CreateCompatibleDC,
|
@@ -67,96 +39,15 @@ module Win32
|
|
67
39
|
attach_function :release_dc, :ReleaseDC,
|
68
40
|
[:long, :long], :int
|
69
41
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
if(searched_window[:search_class] != 0)
|
74
|
-
title = Util.window_class(hwnd)
|
75
|
-
else
|
76
|
-
title = Util.window_title(hwnd)
|
77
|
-
end
|
78
|
-
if title =~ Regexp.new(searched_window[:title].read_string) && window_visible(hwnd)
|
79
|
-
searched_window[:hwnd] = hwnd
|
80
|
-
false
|
81
|
-
else
|
82
|
-
if(searched_window[:search_class] != 0)
|
83
|
-
# if they're searching for a classname, enumerate children, too
|
84
|
-
enum_child_windows(hwnd, EnumWindowCallback, param)
|
85
|
-
if searched_window[:hwnd] != 0
|
86
|
-
# return early if already discovered
|
87
|
-
false
|
88
|
-
else
|
89
|
-
true
|
90
|
-
end
|
91
|
-
else
|
92
|
-
true
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
class WindowStruct < FFI::Struct
|
98
|
-
layout :title, :pointer,
|
99
|
-
:hwnd, :long,
|
100
|
-
:search_class, :char # boolean
|
101
|
-
end
|
102
|
-
|
103
|
-
def hwnd(window_title, search_class = false)
|
104
|
-
window = WindowStruct.new
|
105
|
-
unless window_title.is_a?(Regexp)
|
106
|
-
window_title = Regexp.escape(window_title.to_s)
|
107
|
-
else
|
108
|
-
window_title = window_title.to_s
|
109
|
-
end
|
110
|
-
window_title = FFI::MemoryPointer.from_string(window_title)
|
111
|
-
window[:title] = window_title
|
112
|
-
window[:search_class] = search_class ? 1 : 0
|
113
|
-
enum_windows(EnumWindowCallback, window.to_ptr)
|
114
|
-
window[:hwnd] == 0 ? nil : window[:hwnd]
|
115
|
-
end
|
116
|
-
|
117
|
-
def prepare_window(hwnd, pause)
|
118
|
-
restore(hwnd) if minimized(hwnd)
|
119
|
-
set_foreground(hwnd)
|
120
|
-
sleep pause
|
121
|
-
end
|
122
|
-
|
123
|
-
SW_RESTORE = 9
|
124
|
-
|
125
|
-
def restore(hwnd)
|
126
|
-
show_window(hwnd, SW_RESTORE)
|
127
|
-
end
|
128
|
-
|
129
|
-
def set_foreground(hwnd)
|
130
|
-
if foreground_window != hwnd
|
131
|
-
set_foreground_window(hwnd)
|
132
|
-
set_active_window(hwnd)
|
133
|
-
bring_window_to_top(hwnd)
|
134
|
-
# and just in case...
|
135
|
-
foreground_thread = window_thread_process_id(foreground_window, nil)
|
136
|
-
other_thread = window_thread_process_id(hwnd, nil)
|
137
|
-
attach_thread_input(foreground_thread, other_thread, true) unless other_thread == foreground_thread
|
138
|
-
set_foreground_window(hwnd)
|
139
|
-
set_active_window(hwnd)
|
140
|
-
bring_window_to_top(hwnd)
|
141
|
-
attach_thread_input(foreground_thread, other_thread, false) unless other_thread == foreground_thread
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
def get_process_id_from_hwnd hwnd
|
146
|
-
out = FFI::MemoryPointer.new(:uint)
|
147
|
-
window_thread_process_id(hwnd, out)
|
148
|
-
out.get_uint32(0) # read_uint
|
149
|
-
end
|
150
|
-
|
151
|
-
def capture_all(hwnd, &proc)
|
152
|
-
width, height = Util.dimensions_for(hwnd)
|
153
|
-
capture_area(hwnd, 0, 0, width, height, &proc)
|
42
|
+
def capture_all(hwnd)
|
43
|
+
width, height = dimensions_for(hwnd)
|
44
|
+
capture_area(hwnd, 0, 0, width, height)
|
154
45
|
end
|
155
46
|
|
156
47
|
SRCCOPY = 0x00CC0020
|
157
48
|
DIB_RGB_COLORS = 0
|
158
49
|
|
159
|
-
def capture_area(hwnd, x1, y1, x2, y2)
|
50
|
+
def capture_area(hwnd, x1, y1, x2, y2)
|
160
51
|
hScreenDC = dc(hwnd)
|
161
52
|
w = x2-x1
|
162
53
|
h = y2-y1
|
@@ -181,14 +72,21 @@ module Win32
|
|
181
72
|
54
|
182
73
|
].pack('SLSSL')
|
183
74
|
|
184
|
-
|
185
|
-
yield(w, h, bmp_data)
|
75
|
+
Image.new(bmFileHeader + bmInfo + lpvpxldata.read_string(bitmap_size), w, h)
|
186
76
|
ensure
|
187
77
|
lpvpxldata.free
|
188
78
|
delete_object(hmemBM)
|
189
79
|
delete_dc(hmemDC)
|
190
80
|
release_dc(0, hScreenDC)
|
191
81
|
end
|
82
|
+
|
83
|
+
def dimensions_for(hwnd)
|
84
|
+
rect = [0, 0, 0, 0].pack('L4')
|
85
|
+
BitmapMaker.client_rect(hwnd.to_i, rect)
|
86
|
+
_, _, width, height = rect.unpack('L4')
|
87
|
+
[width, height]
|
88
|
+
end
|
89
|
+
|
192
90
|
end
|
193
91
|
end
|
194
92
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module RAutomation
|
2
|
+
module Adapter
|
3
|
+
module Ffi
|
4
|
+
# Extensions for RAutomation Ffi adapter
|
5
|
+
class Window
|
6
|
+
|
7
|
+
# Searches for the child window of the current window
|
8
|
+
# @param {Window} locators locators for the child window
|
9
|
+
# @return {Window} child window object
|
10
|
+
def child(locators)
|
11
|
+
self.class.new :hwnd => Functions.child_hwnd(hwnd, locators)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Win32
|
2
|
+
module Screenshot
|
3
|
+
# Holds the bitmap data and writes it to the disk
|
4
|
+
class Image
|
5
|
+
# [String] raw bitmap blob
|
6
|
+
attr_reader :bitmap
|
7
|
+
|
8
|
+
# [String] bitmap width
|
9
|
+
attr_reader :width
|
10
|
+
|
11
|
+
# [String] bitmap height
|
12
|
+
attr_reader :height
|
13
|
+
|
14
|
+
# Supported output formats
|
15
|
+
FORMATS = %w{bmp gif jpg png}
|
16
|
+
|
17
|
+
# @private
|
18
|
+
def initialize(blob, width, height)
|
19
|
+
@bitmap = blob
|
20
|
+
@width = width
|
21
|
+
@height = height
|
22
|
+
end
|
23
|
+
|
24
|
+
# Writes image to the disk.
|
25
|
+
# @param [String] file_path writes image to the specified path.
|
26
|
+
# @raise [RuntimeError] when _file_path_ already exists.
|
27
|
+
# @raise [RuntimeError] when _file_path_ is not with the supported output {FORMATS} extension.
|
28
|
+
def write(file_path)
|
29
|
+
raise "File already exists: #{file_path}!" if File.exists? file_path
|
30
|
+
ext = File.extname(file_path)[1..-1]
|
31
|
+
raise "File '#{file_path}' has to have one of the following extensions: #{FORMATS.join(", ")}" unless ext && FORMATS.include?(ext.downcase)
|
32
|
+
|
33
|
+
if ext.downcase == "bmp"
|
34
|
+
File.open(file_path, "wb") {|io| io.write @bitmap}
|
35
|
+
else
|
36
|
+
image = ::MiniMagick::Image.read @bitmap
|
37
|
+
image.format ext
|
38
|
+
image.write file_path
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Win32
|
2
|
+
module Screenshot
|
3
|
+
# Capture Screenshots on Windows with Ruby
|
4
|
+
class Take
|
5
|
+
|
6
|
+
class << self
|
7
|
+
# Takes a screenshot of the specified object or it's area.
|
8
|
+
#
|
9
|
+
# @example Take a screenshot of the window with the specified title
|
10
|
+
# Win32::Screenshot::Take.of(:window, :title => "Windows Internet Explorer")
|
11
|
+
#
|
12
|
+
# @example Take a screenshot of the foreground
|
13
|
+
# Win32::Screenshot::Take.of(:foreground)
|
14
|
+
#
|
15
|
+
# @example Take a screenshot of the specified window's top-left corner's area
|
16
|
+
# Win32::Screenshot::Take.of(:window, :title => /internet/i, :area => [10, 10, 20, 20])
|
17
|
+
#
|
18
|
+
# @example Take a screenshot of the window with the specified handle
|
19
|
+
# Win32::Screenshot::Take.of(:window, :hwnd => 123456)
|
20
|
+
#
|
21
|
+
# @example Take a screenshot of the child window with the specified internal class name
|
22
|
+
# Win32::Screenshot::Take.of(:rautomation, RAutomation::Window.new(:hwnd => 123456).child(:class => "Internet Explorer_Server"))
|
23
|
+
#
|
24
|
+
# @param [Symbol] what the type of the object to take a screenshot of,
|
25
|
+
# possible values are _:foreground_, _:desktop_ and _:window_.
|
26
|
+
# @param [Hash] opts options only needed if _what_ is a _:window_ and/or
|
27
|
+
# only an _:area_ is needed to take as a screenshot. It is possible to specify as many
|
28
|
+
# options as are needed for searching for the unique window. By default first window with
|
29
|
+
# matching identifiers will be taken screenshot of. It is possible to use in addition
|
30
|
+
# to other options a 0-based _:index_ option to search for other windows if multiple
|
31
|
+
# windows match the specified criteria.
|
32
|
+
# @option opts [String, Regexp] :title Title of the window
|
33
|
+
# @option opts [String, Regexp] :text Visible text of the window
|
34
|
+
# @option opts [String, Regexp] :class Internal class name of the window
|
35
|
+
# @option opts [String, Fixnum] :hwnd Window handle in decimal format
|
36
|
+
# @option opts [String, Fixnum] :pid Window process ID (PID)
|
37
|
+
# @option opts [String, Fixnum] :index 0-based index to specify n-th window to take a screenshot of if
|
38
|
+
# all other criteria match
|
39
|
+
# @option opts [RAutomation::Window] :rautomation RAutomation::Window object to take a screenshot of. Useful for
|
40
|
+
# taking screenshots of the child windows
|
41
|
+
# @return [Image] the {Image} of the specified object
|
42
|
+
def of(what, opts = {})
|
43
|
+
valid_whats = [:foreground, :desktop, :window]
|
44
|
+
raise "It is not possible to take a screenshot of '#{what}', possible values are #{valid_whats.join(", ")}" unless valid_whats.include?(what)
|
45
|
+
|
46
|
+
self.send(what, opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
alias_method :new, :of
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def foreground(opts)
|
54
|
+
hwnd = BitmapMaker.foreground_window
|
55
|
+
take_screenshot(hwnd, opts)
|
56
|
+
end
|
57
|
+
|
58
|
+
def desktop(opts)
|
59
|
+
hwnd = BitmapMaker.desktop_window
|
60
|
+
take_screenshot(hwnd, opts)
|
61
|
+
end
|
62
|
+
|
63
|
+
def window(opts)
|
64
|
+
area = {:area => opts.delete(:area)}
|
65
|
+
win = opts[:rautomation] || RAutomation::Window.new(opts)
|
66
|
+
timeout = Time.now + 60
|
67
|
+
until win.active?
|
68
|
+
raise "Failed to set window into focus, unable to take a screenshot!" if Time.now >= timeout
|
69
|
+
win.activate
|
70
|
+
end
|
71
|
+
take_screenshot(win.hwnd, opts.merge(area || {}))
|
72
|
+
end
|
73
|
+
|
74
|
+
def take_screenshot(hwnd, opts)
|
75
|
+
if opts[:area]
|
76
|
+
validate_coordinates(hwnd, *opts[:area])
|
77
|
+
BitmapMaker.capture_area(hwnd, *opts[:area])
|
78
|
+
else
|
79
|
+
BitmapMaker.capture_all(hwnd)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def validate_coordinates(hwnd, x1, y1, x2, y2)
|
84
|
+
specified_coordinates = "x1: #{x1}, y1: #{y1}, x2: #{x2}, y2: #{y2}"
|
85
|
+
if [x1, y1, x2, y2].any? {|c| c < 0}
|
86
|
+
raise "specified coordinates (#{specified_coordinates}) are invalid - cannot be negative!"
|
87
|
+
end
|
88
|
+
|
89
|
+
if x1 >= x2 || y1 >= y2
|
90
|
+
raise "specified coordinates (#{specified_coordinates}) are invalid - cannot be x1 >= x2 or y1 >= y2!"
|
91
|
+
end
|
92
|
+
|
93
|
+
max_width, max_height = BitmapMaker.dimensions_for(hwnd)
|
94
|
+
if x2 > max_width || y2 > max_height
|
95
|
+
raise "specified coordinates (#{specified_coordinates}) are invalid - maximum x2: #{max_width} and y2: #{max_height}!"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -2,14 +2,10 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
2
2
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
3
|
require 'win32/screenshot'
|
4
4
|
require 'rubygems'
|
5
|
-
require '
|
6
|
-
require 'spec/autorun'
|
7
|
-
require 'RMagick'
|
5
|
+
require 'rspec'
|
8
6
|
require 'fileutils'
|
9
7
|
|
10
8
|
module SpecHelper
|
11
|
-
SW_MAXIMIZE = 3
|
12
|
-
SW_MINIMIZE = 6
|
13
9
|
HWND_TOPMOST = -1
|
14
10
|
HWND_NOTOPMOST = -2
|
15
11
|
SWP_NOSIZE = 1
|
@@ -23,55 +19,16 @@ module SpecHelper
|
|
23
19
|
# user32.dll
|
24
20
|
attach_function :set_window_pos, :SetWindowPos,
|
25
21
|
[:long, :long, :int, :int, :int, :int, :int], :bool
|
26
|
-
|
27
|
-
def cleanup
|
28
|
-
FileUtils.rm Dir.glob(File.join(File.dirname(__FILE__), "tmp/*"))
|
29
|
-
end
|
30
22
|
|
31
|
-
def
|
32
|
-
temp_dir
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
img = Magick::Image.from_blob(bmp)
|
37
|
-
png = img[0].to_blob {self.format = 'PNG'}
|
38
|
-
png[0..3].should == "\211PNG"
|
39
|
-
File.open(File.join(temp_dir, "#{file}.png"), "wb") {|io| io.puts(png)} unless file.nil?
|
40
|
-
end
|
41
|
-
|
42
|
-
def wait_for_programs_to_open
|
43
|
-
until Win32::Screenshot::BitmapMaker.hwnd(/Internet Explorer/) &&
|
44
|
-
Win32::Screenshot::BitmapMaker.hwnd(/Notepad/)
|
45
|
-
sleep 0.1
|
46
|
-
end
|
47
|
-
wait_for_calculator_to_open
|
48
|
-
|
49
|
-
# just in case of slow PC
|
50
|
-
sleep 8
|
51
|
-
end
|
52
|
-
|
53
|
-
def wait_for_calculator_to_open
|
54
|
-
until Win32::Screenshot::BitmapMaker.hwnd(/Calculator/)
|
55
|
-
sleep 0.1
|
56
|
-
end
|
57
|
-
# just in case of slow PC
|
58
|
-
sleep 2
|
59
|
-
end
|
60
|
-
|
61
|
-
def maximize title
|
62
|
-
Win32::Screenshot::BitmapMaker.show_window(Win32::Screenshot::BitmapMaker.hwnd(title),
|
63
|
-
SW_MAXIMIZE)
|
64
|
-
sleep 1
|
65
|
-
end
|
66
|
-
|
67
|
-
def minimize title
|
68
|
-
Win32::Screenshot::BitmapMaker.show_window(Win32::Screenshot::BitmapMaker.hwnd(title),
|
69
|
-
SW_MINIMIZE)
|
70
|
-
sleep 1
|
23
|
+
def save_and_verify_image(img, file=nil)
|
24
|
+
FileUtils.mkdir @temp_dir unless File.exists?(@temp_dir)
|
25
|
+
file_name = File.join @temp_dir, "#{file}.bmp"
|
26
|
+
img.write file_name
|
27
|
+
img.bitmap[0..1].should == 'BM'
|
71
28
|
end
|
72
29
|
|
73
30
|
def resize title
|
74
|
-
hwnd =
|
31
|
+
hwnd = RAutomation::Window.new(:title => title).hwnd
|
75
32
|
set_window_pos(hwnd,
|
76
33
|
HWND_TOPMOST,
|
77
34
|
0, 0, 150, 238,
|
@@ -83,3 +40,9 @@ module SpecHelper
|
|
83
40
|
sleep 1
|
84
41
|
end
|
85
42
|
end
|
43
|
+
|
44
|
+
RSpec.configure do |config|
|
45
|
+
config.include(SpecHelper)
|
46
|
+
config.before(:suite) {FileUtils.rm Dir.glob(File.join(File.dirname(__FILE__), "tmp/*"))}
|
47
|
+
config.before(:all) {@temp_dir = File.join(File.dirname(__FILE__), 'tmp')}
|
48
|
+
end
|