win32screenshot 0.0.8 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|