win32screenshot 0.0.8 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,53 +1,25 @@
1
- require 'ffi'
2
-
3
1
  module Win32
4
- class Screenshot
5
- # internal methods
6
- class BitmapMaker #:nodoc:all
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
- EnumWindowCallback = FFI::Function.new(:bool, [ :long, :pointer ], { :convention => :stdcall }) do |hwnd, param|
72
- searched_window = WindowStruct.new param
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) # block
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
- bmp_data = bmFileHeader + bmInfo + lpvpxldata.read_string(bitmap_size)
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,12 @@
1
+ module RAutomation
2
+ module Adapter
3
+ module Ffi
4
+ # @private
5
+ module Functions
6
+ class << self
7
+ alias_method :child_hwnd, :control_hwnd
8
+ end
9
+ end
10
+ end
11
+ end
12
+ 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 'spec'
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 check_image(bmp, file=nil)
32
- temp_dir = File.join(File.dirname(__FILE__), 'tmp')
33
- FileUtils.mkdir temp_dir unless File.exists?(temp_dir)
34
- File.open(File.join(temp_dir, "#{file}.bmp"), "wb") {|io| io.write(bmp)} unless file.nil?
35
- bmp[0..1].should == 'BM'
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 = Win32::Screenshot::BitmapMaker.hwnd(title)
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