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.
@@ -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