dsh-spectacles 0.0.0-stage → 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.
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "dsh-spectacles",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Point at one window and let the agent read it clearly. Window scoped screen reading for DeepSeek Harness: look (numbers first), read_text (OCR), see (crop + zoom handed to the host vision).",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "os": [
9
+ "win32"
10
+ ],
11
+ "exports": {
12
+ ".": "./index.js",
13
+ "./client": "./client.js",
14
+ "./cordis.patch.yml": "./cordis.patch.yml",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "index.js",
19
+ "client.js",
20
+ "cordis.patch.yml",
21
+ "scripts/",
22
+ "README.md",
23
+ "README.zh.md",
24
+ "CHANGELOG.md",
25
+ "LICENSE"
26
+ ],
27
+ "keywords": [
28
+ "deepseek",
29
+ "dsh",
30
+ "plugin",
31
+ "vision",
32
+ "look",
33
+ "spectacles",
34
+ "screenshot",
35
+ "ocr",
36
+ "window-capture"
37
+ ],
38
+ "dsh": {
39
+ "manifestVersion": 1,
40
+ "bundle": {
41
+ "patch": "./cordis.patch.yml"
42
+ },
43
+ "client": {
44
+ "platform": "web",
45
+ "inject": []
46
+ }
47
+ },
48
+ "engines": {
49
+ "node": ">=22"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/yanming119/dsh-spectacles.git"
54
+ },
55
+ "homepage": "https://github.com/yanming119/dsh-spectacles",
56
+ "bugs": {
57
+ "url": "https://github.com/yanming119/dsh-spectacles/issues"
58
+ },
59
+ "author": "yanming119",
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
6
63
  }
@@ -0,0 +1,83 @@
1
+ param(
2
+ [int]$TimeoutSec = 30,
3
+ [string]$Out = ""
4
+ )
5
+ $DataDir = if ($env:DSH_LOOK_DATA) { $env:DSH_LOOK_DATA } else { Join-Path $env:TEMP 'dsh-look' }
6
+ if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
7
+ if (-not $Out) { $Out = Join-Path $DataDir 'picked.json' }
8
+ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
9
+ $ErrorActionPreference = 'Stop'
10
+
11
+ Add-Type -TypeDefinition @"
12
+ using System;
13
+ using System.Text;
14
+ using System.Runtime.InteropServices;
15
+
16
+ public class Pk3 {
17
+ [DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT p);
18
+ [DllImport("user32.dll")] public static extern IntPtr WindowFromPoint(POINT p);
19
+ [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint flags);
20
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
21
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
22
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
23
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
24
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
25
+
26
+ public struct POINT { public int X, Y; }
27
+ public struct RECT { public int Left, Top, Right, Bottom; }
28
+ }
29
+ "@
30
+
31
+ function Get-Title($h) {
32
+ $sb = New-Object System.Text.StringBuilder 512
33
+ [Pk3]::GetWindowText($h, $sb, 512) | Out-Null
34
+ return $sb.ToString()
35
+ }
36
+
37
+ function Emit($obj) {
38
+ $json = $obj | ConvertTo-Json -Compress
39
+ [System.IO.File]::WriteAllText($Out, $json, (New-Object System.Text.UTF8Encoding($false)))
40
+ Write-Output $json
41
+ }
42
+
43
+ $start = [Pk3]::GetForegroundWindow()
44
+ $deadline = (Get-Date).AddSeconds($TimeoutSec)
45
+
46
+ while ((Get-Date) -lt $deadline) {
47
+ $fg = [Pk3]::GetForegroundWindow()
48
+ if ($fg -ne [IntPtr]::Zero -and $fg -ne $start -and [Pk3]::IsWindowVisible($fg) -and (Get-Title $fg).Length -gt 0) {
49
+ # 真正判据:鼠标此刻就在这个窗口上——说明是用户点它,不是别的窗口自己冒出来。
50
+ $pt = New-Object Pk3+POINT
51
+ [Pk3]::GetCursorPos([ref]$pt) | Out-Null
52
+ $under = [Pk3]::WindowFromPoint($pt)
53
+ $root = if ($under -ne [IntPtr]::Zero) { [Pk3]::GetAncestor($under, 2) } else { [IntPtr]::Zero }
54
+ if ($root -eq $fg) {
55
+ # 别把宿主自己(DeepSeek Harness 的窗口)当成目标——它自己会冒到前台
56
+ $procId = 0
57
+ [Pk3]::GetWindowThreadProcessId($fg, [ref]$procId) | Out-Null
58
+ $pname = ''
59
+ try { $pname = (Get-Process -Id ([int]$procId) -ErrorAction Stop).ProcessName } catch { $pname = '' }
60
+ if ($pname -like '*Harness*' -or (Get-Title $fg) -like '*DeepSeek Harness*') {
61
+ Start-Sleep -Milliseconds 60
62
+ continue
63
+ }
64
+ $r = New-Object Pk3+RECT
65
+ [Pk3]::GetWindowRect($fg, [ref]$r) | Out-Null
66
+ $procId = 0
67
+ [Pk3]::GetWindowThreadProcessId($fg, [ref]$procId) | Out-Null
68
+ Emit ([ordered]@{
69
+ ok = $true
70
+ when = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff')
71
+ hwnd = [int]$fg
72
+ title = (Get-Title $fg)
73
+ pid = [int]$procId
74
+ rect = @([int]$r.Left, [int]$r.Top, [int]($r.Right - $r.Left), [int]($r.Bottom - $r.Top))
75
+ })
76
+ exit 0
77
+ }
78
+ }
79
+ Start-Sleep -Milliseconds 60
80
+ }
81
+
82
+ Emit ([ordered]@{ ok = $false; error = 'timeout: no click' })
83
+ exit 1
@@ -0,0 +1,205 @@
1
+ param(
2
+ [int]$TimeoutSec = 60,
3
+ [string]$Out = ""
4
+ )
5
+ $DataDir = if ($env:DSH_LOOK_DATA) { $env:DSH_LOOK_DATA } else { Join-Path $env:TEMP 'dsh-look' }
6
+ if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
7
+ if (-not $Out) { $Out = Join-Path $DataDir 'region.json' }
8
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
9
+ $ErrorActionPreference = 'Stop'
10
+
11
+ Add-Type -AssemblyName System.Windows.Forms
12
+ Add-Type -AssemblyName System.Drawing
13
+ Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @"
14
+ using System;
15
+ using System.Text;
16
+ using System.Runtime.InteropServices;
17
+
18
+ public class Rg {
19
+ [DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
20
+ [DllImport("user32.dll")] public static extern IntPtr WindowFromPoint(POINT p);
21
+ [DllImport("user32.dll")] public static extern IntPtr GetAncestor(IntPtr h, uint flags);
22
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
23
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
24
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
25
+
26
+ public struct POINT { public int X, Y; }
27
+ public struct RECT { public int Left, Top, Right, Bottom; }
28
+ }
29
+ "@
30
+ [void][Rg]::SetProcessDPIAware()
31
+
32
+ $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen
33
+
34
+ # 先冻一帧屏:遮罩下面是这张定格,选框里露出原图,框外压暗 —— 不动就能选得准
35
+ $bg = New-Object System.Drawing.Bitmap $vs.Width, $vs.Height
36
+ $g0 = [System.Drawing.Graphics]::FromImage($bg)
37
+ $g0.CopyFromScreen($vs.Left, $vs.Top, 0, 0, (New-Object System.Drawing.Size $vs.Width, $vs.Height))
38
+ $g0.Dispose()
39
+
40
+ $script:bg = $bg
41
+ $script:sel = $null
42
+ $script:dragging = $false
43
+ $script:done = $false
44
+ $script:cancelled = $false
45
+ $script:start = $null
46
+ $script:dim = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(120, 0, 0, 0))
47
+ $script:pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 0, 205, 120)), 2
48
+ $script:penDot = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(200, 255, 255, 255)), 1
49
+ $script:font = New-Object System.Drawing.Font 'Microsoft YaHei UI', 12, ([System.Drawing.FontStyle]::Bold)
50
+ $script:brText = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::White)
51
+ $script:brBox = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(215, 20, 24, 30))
52
+
53
+ $form = New-Object System.Windows.Forms.Form
54
+ $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
55
+ $form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
56
+ $form.Bounds = New-Object System.Drawing.Rectangle $vs.Left, $vs.Top, $vs.Width, $vs.Height
57
+ $form.TopMost = $true
58
+ $form.ShowInTaskbar = $false
59
+ $form.Cursor = [System.Windows.Forms.Cursors]::Cross
60
+ $form.KeyPreview = $true
61
+ $form.BackColor = [System.Drawing.Color]::Black
62
+ try { $form.GetType().GetProperty('DoubleBuffered', 'Instance,NonPublic').SetValue($form, $true) } catch {}
63
+
64
+ function Draw-Tag($g, $text, $x, $y) {
65
+ $sz = $g.MeasureString($text, $script:font)
66
+ $w = [int]$sz.Width + 16; $h = [int]$sz.Height + 8
67
+ $bx = [Math]::Max(0, [Math]::Min($x, $script:bg.Width - $w))
68
+ $by = [Math]::Max(0, [Math]::Min($y, $script:bg.Height - $h))
69
+ $g.FillRectangle($script:brBox, $bx, $by, $w, $h)
70
+ $g.DrawString($text, $script:font, $script:brText, ($bx + 8), ($by + 4))
71
+ }
72
+
73
+ $form.Add_Paint({
74
+ param($s, $e)
75
+ $g = $e.Graphics
76
+ $g.DrawImageUnscaled($script:bg, 0, 0)
77
+ $r = $script:sel
78
+ if ($r -and $r.Width -gt 0 -and $r.Height -gt 0) {
79
+ $W = $script:bg.Width; $H = $script:bg.Height
80
+ $g.FillRectangle($script:dim, 0, 0, $W, [Math]::Max(0, $r.Top))
81
+ $g.FillRectangle($script:dim, 0, $r.Bottom, $W, [Math]::Max(0, $H - $r.Bottom))
82
+ $g.FillRectangle($script:dim, 0, $r.Top, [Math]::Max(0, $r.Left), $r.Height)
83
+ $g.FillRectangle($script:dim, $r.Right, $r.Top, [Math]::Max(0, $W - $r.Right), $r.Height)
84
+ $g.DrawRectangle($script:pen, $r)
85
+ Draw-Tag $g ("{0} x {1}" -f $r.Width, $r.Height) ($r.Left + 6) ($r.Top + 6)
86
+ }
87
+ else {
88
+ $g.FillRectangle($script:dim, 0, 0, $script:bg.Width, $script:bg.Height)
89
+ Draw-Tag $g '拖一个框;Esc 取消' 40 40
90
+ }
91
+ })
92
+
93
+ $form.Add_MouseDown({
94
+ param($s, $e)
95
+ if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
96
+ $script:start = $e.Location
97
+ $script:sel = New-Object System.Drawing.Rectangle $e.X, $e.Y, 0, 0
98
+ $script:dragging = $true
99
+ $s.Invalidate()
100
+ }
101
+ elseif ($e.Button -eq [System.Windows.Forms.MouseButtons]::Right) {
102
+ $script:cancelled = $true
103
+ $s.Close()
104
+ }
105
+ })
106
+
107
+ $form.Add_MouseMove({
108
+ param($s, $e)
109
+ if (-not $script:dragging) { return }
110
+ $x1 = [Math]::Min($script:start.X, $e.X); $y1 = [Math]::Min($script:start.Y, $e.Y)
111
+ $x2 = [Math]::Max($script:start.X, $e.X); $y2 = [Math]::Max($script:start.Y, $e.Y)
112
+ $script:sel = New-Object System.Drawing.Rectangle $x1, $y1, ($x2 - $x1), ($y2 - $y1)
113
+ $s.Invalidate()
114
+ })
115
+
116
+ $closeTimer = New-Object System.Windows.Forms.Timer
117
+ $closeTimer.Interval = 160
118
+ $closeTimer.Add_Tick({ $closeTimer.Stop(); $form.Close() })
119
+
120
+ $form.Add_MouseUp({
121
+ param($s, $e)
122
+ if (-not $script:dragging) { return }
123
+ $script:dragging = $false
124
+ if ($script:sel -and $script:sel.Width -ge 8 -and $script:sel.Height -ge 8) {
125
+ $script:done = $true
126
+ # 先把遮罩藏掉(人立刻看见框定住了),窗口晚 160ms 再关:
127
+ # 松手这一下要是当场关窗,抬键消息会漏给下面的窗口,有的程序会当成一次点击。
128
+ $s.Visible = $false
129
+ $closeTimer.Start()
130
+ }
131
+ })
132
+
133
+ $form.Add_KeyDown({
134
+ param($s, $e)
135
+ if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Escape) {
136
+ $script:cancelled = $true
137
+ $s.Close()
138
+ }
139
+ })
140
+
141
+ $form.Add_Shown({ $form.Activate() })
142
+
143
+ $timer = New-Object System.Windows.Forms.Timer
144
+ $timer.Interval = [Math]::Max(1000, $TimeoutSec * 1000)
145
+ $timer.Add_Tick({ $script:cancelled = $true; $form.Close() })
146
+ $timer.Start()
147
+
148
+ [System.Windows.Forms.Application]::Run($form)
149
+ $timer.Stop()
150
+
151
+ if (-not $script:done -or -not $script:sel) {
152
+ @{ ok = $false; error = 'cancelled' } | ConvertTo-Json -Compress
153
+ exit 1
154
+ }
155
+
156
+ # 等遮罩真的消失,不然 WindowFromPoint 会点到我们自己
157
+ Start-Sleep -Milliseconds 90
158
+
159
+ $sel = $script:sel
160
+ $sx = $vs.Left + $sel.X
161
+ $sy = $vs.Top + $sel.Y
162
+
163
+ $pt = New-Object Rg+POINT
164
+ $pt.X = $sx + [int]($sel.Width / 2)
165
+ $pt.Y = $sy + [int]($sel.Height / 2)
166
+ $h = [Rg]::WindowFromPoint($pt)
167
+ if ($h -eq [IntPtr]::Zero) { @{ ok = $false; error = 'no window under the box' } | ConvertTo-Json -Compress; exit 1 }
168
+ $root = [Rg]::GetAncestor($h, 2)
169
+ if ($root -eq [IntPtr]::Zero) { $root = $h }
170
+
171
+ $sb = New-Object System.Text.StringBuilder 512
172
+ [Rg]::GetWindowText($root, $sb, 512) | Out-Null
173
+ if ($sb.Length -eq 0) {
174
+ @{ ok = $false; error = '那个位置没有窗口(是桌面空白处),换个地方再框' } | ConvertTo-Json -Compress
175
+ exit 1
176
+ }
177
+ $wr = New-Object Rg+RECT
178
+ [Rg]::GetWindowRect($root, [ref]$wr) | Out-Null
179
+ $procId = 0
180
+ [Rg]::GetWindowThreadProcessId($root, [ref]$procId) | Out-Null
181
+
182
+ # 框可能越出窗口:裁到窗口里面,换算成相对窗口左上角
183
+ $ix = [Math]::Max($sx, $wr.Left); $iy = [Math]::Max($sy, $wr.Top)
184
+ $ir = [Math]::Min($sx + $sel.Width, $wr.Right); $ib = [Math]::Min($sy + $sel.Height, $wr.Bottom)
185
+ $cw = [Math]::Max(1, $ir - $ix)
186
+ $ch = [Math]::Max(1, $ib - $iy)
187
+ $cx = $ix - $wr.Left
188
+ $cy = $iy - $wr.Top
189
+
190
+ $res = [ordered]@{
191
+ ok = $true
192
+ when = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')
193
+ hwnd = [int64]$root
194
+ title = $sb.ToString()
195
+ pid = $procId
196
+ rect = @($wr.Left, $wr.Top, ($wr.Right - $wr.Left), ($wr.Bottom - $wr.Top))
197
+ screen = @($sx, $sy, $sel.Width, $sel.Height)
198
+ crop = @($cx, $cy, $cw, $ch)
199
+ }
200
+
201
+ $dir = Split-Path $Out -Parent
202
+ if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
203
+ [System.IO.File]::WriteAllText($Out, ($res | ConvertTo-Json -Compress), (New-Object System.Text.UTF8Encoding($false)))
204
+
205
+ $res | ConvertTo-Json -Compress
@@ -0,0 +1,133 @@
1
+ param(
2
+ [string]$Crop = "",
3
+ [double]$Scale = 1.0,
4
+ [switch]$Image,
5
+ [string]$Out = "",
6
+ [string]$SaveDir = "",
7
+ [int]$TimeoutMs = 6000
8
+ )
9
+ $DataDir = if ($env:DSH_LOOK_DATA) { $env:DSH_LOOK_DATA } else { Join-Path $env:TEMP 'dsh-look' }
10
+ if (-not (Test-Path $DataDir)) { New-Item -ItemType Directory -Force -Path $DataDir | Out-Null }
11
+ if (-not $SaveDir) { $SaveDir = $DataDir }
12
+
13
+ $ErrorActionPreference = 'Stop'
14
+ Add-Type -AssemblyName System.Drawing
15
+
16
+ # fullscreen capture defaults to GDI: fast (~200ms) and needs no external binary.
17
+ # set DSH_LOOK_FFMPEG only to force the ffmpeg/DXGI path (slower, but sees some hardware overlays).
18
+ $ffmpeg = $env:DSH_LOOK_FFMPEG
19
+ if ($ffmpeg -and -not (Test-Path $ffmpeg)) { $ffmpeg = '' }
20
+
21
+ if (-not (Test-Path $SaveDir)) { New-Item -ItemType Directory -Path $SaveDir -Force | Out-Null }
22
+ $stamp = Get-Date -Format 'yyyyMMdd-HHmmss-fff'
23
+ $tmp = Join-Path $SaveDir "raw-$stamp.png"
24
+ $stateFile = Join-Path $SaveDir 'last-grid.txt'
25
+
26
+ $sw = [Diagnostics.Stopwatch]::StartNew()
27
+ $source = 'gdi-copyfromscreen'
28
+ if ($ffmpeg) {
29
+ $source = 'dxgi-ddagrabb'
30
+ & $ffmpeg -hide_banner -loglevel error -f lavfi -i "ddagrab=0:framerate=1" -vf "hwdownload,format=bgra" -frames:v 1 -y $tmp 2>$null | Out-Null
31
+ } else {
32
+ # GDI screen copy (DPI-aware, no external binary)
33
+ if (-not ('YM.Dpi' -as [type])) {
34
+ Add-Type -Namespace YM -Name Dpi -MemberDefinition '[DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] public static extern int GetSystemMetrics(int n);'
35
+ }
36
+ [void][YM.Dpi]::SetProcessDPIAware()
37
+ $sw0 = [YM.Dpi]::GetSystemMetrics(0); $sh0 = [YM.Dpi]::GetSystemMetrics(1)
38
+ $shot = New-Object System.Drawing.Bitmap $sw0, $sh0
39
+ $g0 = [System.Drawing.Graphics]::FromImage($shot)
40
+ $g0.CopyFromScreen(0, 0, 0, 0, (New-Object System.Drawing.Size $sw0, $sh0))
41
+ $g0.Dispose()
42
+ $shot.Save($tmp, [System.Drawing.Imaging.ImageFormat]::Png)
43
+ $shot.Dispose()
44
+ }
45
+ $sw.Stop()
46
+ if (-not (Test-Path $tmp)) { Write-Output '{"ok":false,"error":"capture failed"}'; exit 1 }
47
+
48
+ $src = [System.Drawing.Bitmap]::FromFile($tmp)
49
+ $screenW = $src.Width
50
+ $screenH = $src.Height
51
+
52
+ $x = 0; $y = 0; $w = $screenW; $h = $screenH
53
+ if ($Crop) {
54
+ $p = $Crop -split ','
55
+ if ($p.Length -eq 4) {
56
+ $x = [int]$p[0]; $y = [int]$p[1]; $w = [int]$p[2]; $h = [int]$p[3]
57
+ if ($x -lt 0) { $x = 0 }
58
+ if ($y -lt 0) { $y = 0 }
59
+ if (($x + $w) -gt $screenW) { $w = $screenW - $x }
60
+ if (($y + $h) -gt $screenH) { $h = $screenH - $y }
61
+ }
62
+ }
63
+
64
+ function New-StatBitmap($bmp, $tw, $th) {
65
+ $small = New-Object System.Drawing.Bitmap $tw, $th
66
+ $g = [System.Drawing.Graphics]::FromImage($small)
67
+ $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBilinear
68
+ $g.DrawImage($bmp, (New-Object System.Drawing.Rectangle 0, 0, $tw, $th), (New-Object System.Drawing.Rectangle $x, $y, $w, $h), [System.Drawing.GraphicsUnit]::Pixel)
69
+ $g.Dispose()
70
+ return $small
71
+ }
72
+
73
+ $stat = New-StatBitmap $src 64 36
74
+ $gw = $stat.Width; $gh = $stat.Height
75
+ $gray = New-Object 'int[]' ($gw * $gh)
76
+ $sumR = 0; $sumG = 0; $sumB = 0
77
+ for ($yy = 0; $yy -lt $gh; $yy++) {
78
+ for ($xx = 0; $xx -lt $gw; $xx++) {
79
+ $c = $stat.GetPixel($xx, $yy)
80
+ $sumR += $c.R; $sumG += $c.G; $sumB += $c.B
81
+ $gray[$yy * $gw + $xx] = [int](($c.R * 0.299) + ($c.G * 0.587) + ($c.B * 0.114))
82
+ }
83
+ }
84
+ $n = $gw * $gh
85
+ $avgR = [int]($sumR / $n); $avgG = [int]($sumG / $n); $avgB = [int]($sumB / $n)
86
+
87
+ $edges = 0
88
+ for ($yy = 1; $yy -lt $gh - 1; $yy++) {
89
+ for ($xx = 1; $xx -lt $gw - 1; $xx++) {
90
+ $i = $yy * $gw + $xx
91
+ $dx = [Math]::Abs($gray[$i + 1] - $gray[$i - 1])
92
+ $dy = [Math]::Abs($gray[$i + $gw] - $gray[$i - $gw])
93
+ if (($dx + $dy) -gt 60) { $edges++ }
94
+ }
95
+ }
96
+ $edgeDensity = [Math]::Round($edges / $n, 4)
97
+
98
+ $cur = ($gray | ForEach-Object { if ($_ -lt 10) { '00' } elseif ($_ -lt 100) { '0' + $_ } else { "$_" } }) -join ''
99
+ $changed = $null
100
+ if (Test-Path $stateFile) { $prev = Get-Content $stateFile -Raw; if ($prev.Trim() -eq $cur) { $changed = $false } else { $changed = $true } }
101
+ Set-Content -Path $stateFile -Value $cur -NoNewline
102
+
103
+ $stat.Dispose()
104
+
105
+ $pngPath = ''
106
+ if ($Image -or $Out) {
107
+ $target = New-Object System.Drawing.Bitmap ([int]($w * $Scale)), ([int]($h * $Scale))
108
+ $g2 = [System.Drawing.Graphics]::FromImage($target)
109
+ $g2.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
110
+ $g2.DrawImage($src, (New-Object System.Drawing.Rectangle 0, 0, $target.Width, $target.Height), (New-Object System.Drawing.Rectangle $x, $y, $w, $h), [System.Drawing.GraphicsUnit]::Pixel)
111
+ $g2.Dispose()
112
+ if ($Out) { $pngPath = $Out } else { $pngPath = Join-Path $SaveDir "look-$stamp.png" }
113
+ $target.Save($pngPath, [System.Drawing.Imaging.ImageFormat]::Png)
114
+ $target.Dispose()
115
+ }
116
+
117
+ $src.Dispose()
118
+ Remove-Item $tmp -Force -ErrorAction SilentlyContinue
119
+
120
+ $obj = [ordered]@{
121
+ ok = $true
122
+ when = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff')
123
+ source = $source
124
+ screen = @{ w = $screenW; h = $screenH }
125
+ region = @{ x = $x; y = $y; w = $w; h = $h }
126
+ captureMs = $sw.ElapsedMilliseconds
127
+ avgColor = ('#{0:X2}{1:X2}{2:X2}' -f $avgR, $avgG, $avgB)
128
+ edgeDensity = $edgeDensity
129
+ changed = $changed
130
+ grid = "$gw" + 'x' + "$gh"
131
+ png = $pngPath
132
+ }
133
+ $obj | ConvertTo-Json -Compress -Depth 4