@momasiku/dsh-pilot 0.5.7 → 0.5.8
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.
- package/LICENSE +21 -21
- package/README.md +514 -466
- package/cordis.patch.yml +19 -19
- package/lib/index.js +1567 -1567
- package/lib/scripts/_dsh-capture.ps1 +266 -240
- package/lib/scripts/_dsh-win32.ps1 +279 -213
- package/package.json +58 -58
|
@@ -1,241 +1,267 @@
|
|
|
1
|
-
# pilot / _dsh-capture.ps1 - one capture, executed in whatever process called it.
|
|
2
|
-
#
|
|
3
|
-
# Extracted from desktop-probe.ps1 so the same code serves two callers:
|
|
4
|
-
# * desktop-probe.ps1 - one frame per process (the fallback path);
|
|
5
|
-
# * desktop-worker.ps1 - frames in the warm process (the fast path).
|
|
6
|
-
#
|
|
7
|
-
# Differences from the one-shot script it came from: failures throw instead of
|
|
8
|
-
# printing JSON and exiting, and the result is returned instead of being printed.
|
|
9
|
-
$ErrorActionPreference = 'Stop'
|
|
10
|
-
|
|
11
|
-
$script:CaptureStopPayload = $null
|
|
12
|
-
|
|
13
|
-
function Write-ProbeError([string] $message) {
|
|
14
|
-
$script:CaptureStopPayload = [ordered]@{ ok = $false; error = $message }
|
|
15
|
-
throw [System.Exception]::new($message)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function Get-ProcessNameSafe([uint32] $pidValue) {
|
|
19
|
-
try { return (Get-Process -Id $pidValue -ErrorAction Stop).ProcessName } catch { return $null }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function ConvertTo-WindowInfo([IntPtr] $handle, [bool] $withProcess) {
|
|
23
|
-
if ($handle -eq [IntPtr]::Zero) { return $null }
|
|
24
|
-
$title = [DshProbe]::WindowTitle($handle)
|
|
25
|
-
$class = [DshProbe]::WindowClass($handle)
|
|
26
|
-
$rect = [DshProbe]::WindowRect($handle)
|
|
27
|
-
$pidValue = [uint32] 0
|
|
28
|
-
$null = [DshProbe]::GetWindowThreadProcessId($handle, [ref] $pidValue)
|
|
29
|
-
return [ordered]@{
|
|
30
|
-
handle = ('0x{0:X}' -f $handle.ToInt64())
|
|
31
|
-
title = $title
|
|
32
|
-
class = $class
|
|
33
|
-
process = if ($withProcess) { Get-ProcessNameSafe $pidValue } else { $null }
|
|
34
|
-
pid = [int] $pidValue
|
|
35
|
-
bounds = ('{0},{1},{2},{3}' -f $rect.Left, $rect.Top, ($rect.Right - $rect.Left), ($rect.Bottom - $rect.Top))
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<#
|
|
41
|
-
Capture the desktop, a monitor, a region or one window. $Params carries out,
|
|
42
|
-
screen, window, region and includeCursor.
|
|
43
|
-
#>
|
|
44
|
-
function Invoke-DshCapture {
|
|
45
|
-
param([object] $Params)
|
|
46
|
-
|
|
47
|
-
$params = if ($null -eq $Params) { [pscustomobject]@{} } else { $Params }
|
|
48
|
-
|
|
49
|
-
$outPath = if ($params.PSObject.Properties.Name -contains 'out') { [string] $params.out } else { '' }
|
|
50
|
-
if ([string]::IsNullOrWhiteSpace($outPath)) { Write-ProbeError 'params.out (the PNG destination) is required' }
|
|
51
|
-
|
|
52
|
-
$screenMode = if ($params.PSObject.Properties.Name -contains 'screen' -and $null -ne $params.screen) { [string] $params.screen } else { 'primary' }
|
|
53
|
-
$windowQuery = if ($params.PSObject.Properties.Name -contains 'window' -and $null -ne $params.window) { [string] $params.window } else { '' }
|
|
54
|
-
$regionSpec = if ($params.PSObject.Properties.Name -contains 'region' -and $null -ne $params.region) { [string] $params.region } else { '' }
|
|
55
|
-
$includeCursor = [bool] ($params.PSObject.Properties.Name -contains 'includeCursor' -and $params.includeCursor)
|
|
56
|
-
$rulers = $true
|
|
57
|
-
if ($params.PSObject.Properties.Name -contains 'rulers') { $rulers = [bool] $params.rulers }
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# ------------------------------------------------------------ screen inventory
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
$
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
$
|
|
82
|
-
$
|
|
83
|
-
$
|
|
84
|
-
|
|
85
|
-
if (-
|
|
86
|
-
$
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
$
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
$
|
|
107
|
-
$
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
$
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
$
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
$
|
|
126
|
-
$
|
|
127
|
-
$
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
$
|
|
203
|
-
$
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
cursor
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
1
|
+
# pilot / _dsh-capture.ps1 - one capture, executed in whatever process called it.
|
|
2
|
+
#
|
|
3
|
+
# Extracted from desktop-probe.ps1 so the same code serves two callers:
|
|
4
|
+
# * desktop-probe.ps1 - one frame per process (the fallback path);
|
|
5
|
+
# * desktop-worker.ps1 - frames in the warm process (the fast path).
|
|
6
|
+
#
|
|
7
|
+
# Differences from the one-shot script it came from: failures throw instead of
|
|
8
|
+
# printing JSON and exiting, and the result is returned instead of being printed.
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
|
|
11
|
+
$script:CaptureStopPayload = $null
|
|
12
|
+
|
|
13
|
+
function Write-ProbeError([string] $message) {
|
|
14
|
+
$script:CaptureStopPayload = [ordered]@{ ok = $false; error = $message }
|
|
15
|
+
throw [System.Exception]::new($message)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Get-ProcessNameSafe([uint32] $pidValue) {
|
|
19
|
+
try { return (Get-Process -Id $pidValue -ErrorAction Stop).ProcessName } catch { return $null }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ConvertTo-WindowInfo([IntPtr] $handle, [bool] $withProcess) {
|
|
23
|
+
if ($handle -eq [IntPtr]::Zero) { return $null }
|
|
24
|
+
$title = [DshProbe]::WindowTitle($handle)
|
|
25
|
+
$class = [DshProbe]::WindowClass($handle)
|
|
26
|
+
$rect = [DshProbe]::WindowRect($handle)
|
|
27
|
+
$pidValue = [uint32] 0
|
|
28
|
+
$null = [DshProbe]::GetWindowThreadProcessId($handle, [ref] $pidValue)
|
|
29
|
+
return [ordered]@{
|
|
30
|
+
handle = ('0x{0:X}' -f $handle.ToInt64())
|
|
31
|
+
title = $title
|
|
32
|
+
class = $class
|
|
33
|
+
process = if ($withProcess) { Get-ProcessNameSafe $pidValue } else { $null }
|
|
34
|
+
pid = [int] $pidValue
|
|
35
|
+
bounds = ('{0},{1},{2},{3}' -f $rect.Left, $rect.Top, ($rect.Right - $rect.Left), ($rect.Bottom - $rect.Top))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
<#
|
|
41
|
+
Capture the desktop, a monitor, a region or one window. $Params carries out,
|
|
42
|
+
screen, window, region and includeCursor.
|
|
43
|
+
#>
|
|
44
|
+
function Invoke-DshCapture {
|
|
45
|
+
param([object] $Params)
|
|
46
|
+
|
|
47
|
+
$params = if ($null -eq $Params) { [pscustomobject]@{} } else { $Params }
|
|
48
|
+
|
|
49
|
+
$outPath = if ($params.PSObject.Properties.Name -contains 'out') { [string] $params.out } else { '' }
|
|
50
|
+
if ([string]::IsNullOrWhiteSpace($outPath)) { Write-ProbeError 'params.out (the PNG destination) is required' }
|
|
51
|
+
|
|
52
|
+
$screenMode = if ($params.PSObject.Properties.Name -contains 'screen' -and $null -ne $params.screen) { [string] $params.screen } else { 'primary' }
|
|
53
|
+
$windowQuery = if ($params.PSObject.Properties.Name -contains 'window' -and $null -ne $params.window) { [string] $params.window } else { '' }
|
|
54
|
+
$regionSpec = if ($params.PSObject.Properties.Name -contains 'region' -and $null -ne $params.region) { [string] $params.region } else { '' }
|
|
55
|
+
$includeCursor = [bool] ($params.PSObject.Properties.Name -contains 'includeCursor' -and $params.includeCursor)
|
|
56
|
+
$rulers = $true
|
|
57
|
+
if ($params.PSObject.Properties.Name -contains 'rulers') { $rulers = [bool] $params.rulers }
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------ screen inventory
|
|
61
|
+
# Re-enumerated on every capture, deliberately.
|
|
62
|
+
#
|
|
63
|
+
# System.Windows.Forms.Screen.AllScreens is a static cache that only refreshes
|
|
64
|
+
# when the process pumps WM_DISPLAYCHANGE. The warm worker is a -NonInteractive
|
|
65
|
+
# read loop with no message pump, so that cache is never invalidated: after a
|
|
66
|
+
# resolution change the worker keeps reporting the geometry it saw at startup.
|
|
67
|
+
# Measured here - a worker alive 13 hours reported
|
|
68
|
+
# <screens>primary WinDisc 0,0,1600,900</screens> while the same result carried
|
|
69
|
+
# <image_size>1024x768</image_size>. Restarting the worker produced the correct
|
|
70
|
+
# 1024x768. Two answers to one question, inside the very result the coordinate
|
|
71
|
+
# contract is read from. EnumDisplayMonitors re-queries per call, so there is no
|
|
72
|
+
# cache to go stale.
|
|
73
|
+
$screens = @()
|
|
74
|
+
$screenBounds = @()
|
|
75
|
+
$rows = @()
|
|
76
|
+
try {
|
|
77
|
+
$rows = @([DshProbe]::Monitors())
|
|
78
|
+
} catch {
|
|
79
|
+
Write-ProbeError "the desktop is not available to this session (screen enumeration failed: $($_.Exception.Message))"
|
|
80
|
+
}
|
|
81
|
+
foreach ($row in $rows) {
|
|
82
|
+
$parts = $row -split '\|'
|
|
83
|
+
if ($parts.Count -lt 6) { continue }
|
|
84
|
+
$left = [int] $parts[2]; $top = [int] $parts[3]; $right = [int] $parts[4]; $bottom = [int] $parts[5]
|
|
85
|
+
if (($right - $left) -le 0 -or ($bottom - $top) -le 0) { continue }
|
|
86
|
+
$screens += [ordered]@{
|
|
87
|
+
device = $parts[0]
|
|
88
|
+
primary = ($parts[1] -eq '1')
|
|
89
|
+
bounds = ('{0},{1},{2},{3}' -f $left, $top, ($right - $left), ($bottom - $top))
|
|
90
|
+
}
|
|
91
|
+
$screenBounds += [pscustomobject]@{ X = $left; Y = $top; Width = ($right - $left); Height = ($bottom - $top) }
|
|
92
|
+
}
|
|
93
|
+
if ($screens.Count -eq 0) { Write-ProbeError 'the desktop is not available to this session (no interactive display)' }
|
|
94
|
+
|
|
95
|
+
$primaryIndex = 0
|
|
96
|
+
for ($i = 0; $i -lt $screens.Count; $i++) { if ($screens[$i].primary) { $primaryIndex = $i; break } }
|
|
97
|
+
$primaryDip = $screenBounds[$primaryIndex]
|
|
98
|
+
|
|
99
|
+
# Resolution order: an explicit window, then a region, then the screen mode.
|
|
100
|
+
$kind = $null
|
|
101
|
+
$captureDip = $null
|
|
102
|
+
$scale = $null
|
|
103
|
+
|
|
104
|
+
if (-not [string]::IsNullOrWhiteSpace($windowQuery)) {
|
|
105
|
+
$needle = $windowQuery.ToLowerInvariant()
|
|
106
|
+
$candidate = $null
|
|
107
|
+
foreach ($process in (Get-Process | Where-Object { $_.MainWindowHandle -ne 0 })) {
|
|
108
|
+
$handle = $process.MainWindowHandle
|
|
109
|
+
if (-not [DshProbe]::IsWindowVisible($handle)) { continue }
|
|
110
|
+
$title = [DshProbe]::WindowTitle($handle)
|
|
111
|
+
if ($title.ToLowerInvariant().Contains($needle) -or $process.ProcessName.ToLowerInvariant().Contains($needle)) {
|
|
112
|
+
if ($null -eq $candidate) { $candidate = $process }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if ($null -eq $candidate) { Write-ProbeError "no visible top-level window matches '$windowQuery'" }
|
|
116
|
+
$handle = $candidate.MainWindowHandle
|
|
117
|
+
$rect = [DshProbe]::WindowRect($handle)
|
|
118
|
+
$captureDip = [pscustomobject]@{ X = $rect.Left; Y = $rect.Top; Width = ($rect.Right - $rect.Left); Height = ($rect.Bottom - $rect.Top) }
|
|
119
|
+
$kind = 'window'
|
|
120
|
+
} elseif (-not [string]::IsNullOrWhiteSpace($regionSpec)) {
|
|
121
|
+
$parts = @($regionSpec -split '[,\s]+' | Where-Object { $_ -ne '' })
|
|
122
|
+
if ($parts.Count -ne 4) { Write-ProbeError "region must be 'x,y,width,height' (got '$regionSpec')" }
|
|
123
|
+
try { $r = @($parts | ForEach-Object { [int] $_ }) } catch { Write-ProbeError "region must contain four integers (got '$regionSpec')" }
|
|
124
|
+
if ($r[2] -le 0 -or $r[3] -le 0) { Write-ProbeError 'region width and height must be positive' }
|
|
125
|
+
$captureDip = [pscustomobject]@{ X = $r[0]; Y = $r[1]; Width = $r[2]; Height = $r[3] }
|
|
126
|
+
$kind = 'region'
|
|
127
|
+
} elseif ($screenMode -eq 'all') {
|
|
128
|
+
# SM_XVIRTUALSCREEN(76), SM_YVIRTUALSCREEN(77), SM_CXVIRTUALSCREEN(78), SM_CYVIRTUALSCREEN(79).
|
|
129
|
+
# GetSystemMetrics is a live call; SystemInformation.VirtualScreen comes from the
|
|
130
|
+
# same WinForms cache as Screen.AllScreens and goes stale for the same reason.
|
|
131
|
+
$vx = [DshProbe]::GetSystemMetrics(76)
|
|
132
|
+
$vy = [DshProbe]::GetSystemMetrics(77)
|
|
133
|
+
$vw = [DshProbe]::GetSystemMetrics(78)
|
|
134
|
+
$vh = [DshProbe]::GetSystemMetrics(79)
|
|
135
|
+
if ($vw -le 0 -or $vh -le 0) { $vx = $primaryDip.X; $vy = $primaryDip.Y; $vw = $primaryDip.Width; $vh = $primaryDip.Height }
|
|
136
|
+
$captureDip = [pscustomobject]@{ X = $vx; Y = $vy; Width = $vw; Height = $vh }
|
|
137
|
+
$kind = 'all'
|
|
138
|
+
} elseif ($screenMode -eq 'primary') {
|
|
139
|
+
$captureDip = $primaryDip
|
|
140
|
+
$kind = 'primary'
|
|
141
|
+
} else {
|
|
142
|
+
$index = 0
|
|
143
|
+
if (-not [int]::TryParse($screenMode, [ref] $index)) { Write-ProbeError "screen must be 'primary', 'all', or a monitor index (got '$screenMode')" }
|
|
144
|
+
if ($index -lt 0 -or $index -ge $screens.Count) { Write-ProbeError "monitor index $index is out of range (0..$($screens.Count - 1))" }
|
|
145
|
+
$captureDip = $screenBounds[$index]
|
|
146
|
+
$kind = "screen:$index"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# --------------------------------------------------------------------- capture
|
|
150
|
+
Add-Type -AssemblyName System.Drawing
|
|
151
|
+
$bitmap = New-Object System.Drawing.Bitmap($captureDip.Width, $captureDip.Height)
|
|
152
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
153
|
+
$imageWidth = 0
|
|
154
|
+
$imageHeight = 0
|
|
155
|
+
try {
|
|
156
|
+
$graphics.CopyFromScreen($captureDip.X, $captureDip.Y, 0, 0, $bitmap.Size)
|
|
157
|
+
if ($includeCursor) {
|
|
158
|
+
try {
|
|
159
|
+
$cursor = [DshProbe]::Cursor()
|
|
160
|
+
$cx = $cursor.X - $captureDip.X
|
|
161
|
+
$cy = $cursor.Y - $captureDip.Y
|
|
162
|
+
if ($cx -ge 0 -and $cy -ge 0 -and $cx -lt $bitmap.Width -and $cy -lt $bitmap.Height) {
|
|
163
|
+
$pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 255, 0, 0)), 2
|
|
164
|
+
$graphics.DrawLine($pen, ($cx - 14), $cy, ($cx + 14), $cy)
|
|
165
|
+
$graphics.DrawLine($pen, $cx, ($cy - 14), $cx, ($cy + 14))
|
|
166
|
+
$pen.Dispose()
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
# A cursor overlay is cosmetic: never fail a capture over it.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if ($rulers) {
|
|
173
|
+
# Coordinate rulers: ticks every $rulerStep pixels along the top and left
|
|
174
|
+
# edge, each labelled with the SCREEN coordinate it sits at (capture origin
|
|
175
|
+
# plus the local offset). The model can then read a position straight off
|
|
176
|
+
# the image instead of counting pixels, which also survives any later
|
|
177
|
+
# downscaling by the attachment store - the ratio in the envelope says what
|
|
178
|
+
# the labels have to be multiplied by.
|
|
179
|
+
try {
|
|
180
|
+
$rulerStep = if ($bitmap.Width -ge 800) { 200 } else { 100 }
|
|
181
|
+
$font = New-Object System.Drawing.Font('Segoe UI', 11, [System.Drawing.FontStyle]::Bold)
|
|
182
|
+
$ink = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(255, 255, 64, 64))
|
|
183
|
+
$shadow = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(200, 0, 0, 0))
|
|
184
|
+
$tick = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(170, 255, 64, 64)), 1
|
|
185
|
+
try {
|
|
186
|
+
for ($x = $rulerStep; $x -lt $bitmap.Width; $x += $rulerStep) {
|
|
187
|
+
$graphics.DrawLine($tick, $x, 0, $x, 10)
|
|
188
|
+
$label = [string] ([int] $captureDip.X + $x)
|
|
189
|
+
$graphics.DrawString($label, $font, $shadow, ($x + 2), 13)
|
|
190
|
+
$graphics.DrawString($label, $font, $ink, ($x + 1), 12)
|
|
191
|
+
}
|
|
192
|
+
for ($y = $rulerStep; $y -lt $bitmap.Height; $y += $rulerStep) {
|
|
193
|
+
$graphics.DrawLine($tick, 0, $y, 10, $y)
|
|
194
|
+
$label = [string] ([int] $captureDip.Y + $y)
|
|
195
|
+
$graphics.DrawString($label, $font, $shadow, 13, ($y + 2))
|
|
196
|
+
$graphics.DrawString($label, $font, $ink, 12, ($y + 1))
|
|
197
|
+
}
|
|
198
|
+
$originLabel = ('origin {0},{1} step {2}' -f [int] $captureDip.X, [int] $captureDip.Y, $rulerStep)
|
|
199
|
+
$graphics.DrawString($originLabel, $font, $shadow, 15, 30)
|
|
200
|
+
$graphics.DrawString($originLabel, $font, $ink, 14, 29)
|
|
201
|
+
} finally {
|
|
202
|
+
$tick.Dispose()
|
|
203
|
+
$font.Dispose()
|
|
204
|
+
$ink.Dispose()
|
|
205
|
+
$shadow.Dispose()
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
# Rulers are an aid: never fail a capture over them.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
$directory = Split-Path -Parent $outPath
|
|
212
|
+
if (-not [string]::IsNullOrEmpty($directory) -and -not (Test-Path -LiteralPath $directory)) {
|
|
213
|
+
$null = New-Item -ItemType Directory -Path $directory -Force
|
|
214
|
+
}
|
|
215
|
+
$bitmap.Save($outPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
216
|
+
$imageWidth = $bitmap.Width
|
|
217
|
+
$imageHeight = $bitmap.Height
|
|
218
|
+
} catch {
|
|
219
|
+
Write-ProbeError "screen capture failed: $($_.Exception.Message)"
|
|
220
|
+
} finally {
|
|
221
|
+
$graphics.Dispose()
|
|
222
|
+
$bitmap.Dispose()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# DIP -> physical. Both the capture rect and the DIP screen rect come from the
|
|
226
|
+
# same monitor, so the ratio is the monitor's real scale factor.
|
|
227
|
+
if ($captureDip.Width -gt 0) { $scale = [math]::Round(($imageWidth / $captureDip.Width), 4) } else { $scale = 1 }
|
|
228
|
+
$originX = [int] [math]::Round($captureDip.X * $scale)
|
|
229
|
+
$originY = [int] [math]::Round($captureDip.Y * $scale)
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------------- foreground
|
|
232
|
+
$foreground = ConvertTo-WindowInfo ([DshProbe]::GetForegroundWindow()) $true
|
|
233
|
+
$cursorInfo = $null
|
|
234
|
+
try {
|
|
235
|
+
$cursor = [DshProbe]::Cursor()
|
|
236
|
+
$screen = $screens | Where-Object {
|
|
237
|
+
$b = ($_.bounds -split ',') | ForEach-Object { [int] $_ }
|
|
238
|
+
$cursor.X -ge $b[0] -and $cursor.X -lt ($b[0] + $b[2]) -and $cursor.Y -ge $b[1] -and $cursor.Y -lt ($b[1] + $b[3])
|
|
239
|
+
} | Select-Object -First 1
|
|
240
|
+
$cursorInfo = [ordered]@{
|
|
241
|
+
x = $cursor.X
|
|
242
|
+
y = $cursor.Y
|
|
243
|
+
screen = if ($null -ne $screen) { $screen.device } else { $null }
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
$cursorInfo = $null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
$result = [ordered]@{
|
|
250
|
+
ok = $true
|
|
251
|
+
path = (Resolve-Path -LiteralPath $outPath).Path
|
|
252
|
+
kind = $kind
|
|
253
|
+
imageWidth = $imageWidth
|
|
254
|
+
imageHeight = $imageHeight
|
|
255
|
+
originX = $originX
|
|
256
|
+
originY = $originY
|
|
257
|
+
scale = $scale
|
|
258
|
+
dpiAware = $true
|
|
259
|
+
dipBounds = ('{0},{1},{2},{3}' -f $captureDip.X, $captureDip.Y, $captureDip.Width, $captureDip.Height)
|
|
260
|
+
screens = $screens
|
|
261
|
+
cursor = $cursorInfo
|
|
262
|
+
foregroundWindow = $foreground
|
|
263
|
+
rulers = [bool] $rulers
|
|
264
|
+
capturedAt = [DateTimeOffset]::Now.ToString('o')
|
|
265
|
+
}
|
|
266
|
+
return $result
|
|
241
267
|
}
|