dsh-spectacles 0.0.0-stage → 1.1.2

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,65 @@
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.1.2",
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
+ "deepseek-harness",
30
+ "dsh",
31
+ "dsh-plugin",
32
+ "plugin",
33
+ "vision",
34
+ "look",
35
+ "spectacles",
36
+ "screenshot",
37
+ "ocr",
38
+ "window-capture"
39
+ ],
40
+ "dsh": {
41
+ "manifestVersion": 1,
42
+ "bundle": {
43
+ "patch": "./cordis.patch.yml"
44
+ },
45
+ "client": {
46
+ "platform": "web",
47
+ "inject": []
48
+ }
49
+ },
50
+ "engines": {
51
+ "node": ">=22"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/yanming119/dsh-spectacles.git"
56
+ },
57
+ "homepage": "https://github.com/yanming119/dsh-spectacles",
58
+ "bugs": {
59
+ "url": "https://github.com/yanming119/dsh-spectacles/issues"
60
+ },
61
+ "author": "yanming119",
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
6
65
  }
@@ -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,195 @@
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
+ # 用来接你的拖动、记下矩形坐标。框选本身不产生任何画面内容。
36
+ $script:W = $vs.Width
37
+ $script:H = $vs.Height
38
+ $script:sel = $null
39
+ $script:dragging = $false
40
+ $script:done = $false
41
+ $script:cancelled = $false
42
+ $script:start = $null
43
+ $script:pen = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(255, 0, 205, 120)), 2
44
+ $script:penDot = New-Object System.Drawing.Pen ([System.Drawing.Color]::FromArgb(200, 255, 255, 255)), 1
45
+ $script:font = New-Object System.Drawing.Font 'Microsoft YaHei UI', 12, ([System.Drawing.FontStyle]::Bold)
46
+ $script:brText = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::White)
47
+ $script:brBox = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(215, 20, 24, 30))
48
+
49
+ $form = New-Object System.Windows.Forms.Form
50
+ $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None
51
+ $form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
52
+ $form.Bounds = New-Object System.Drawing.Rectangle $vs.Left, $vs.Top, $vs.Width, $vs.Height
53
+ $form.TopMost = $true
54
+ $form.ShowInTaskbar = $false
55
+ $form.Cursor = [System.Windows.Forms.Cursors]::Cross
56
+ $form.KeyPreview = $true
57
+ $form.BackColor = [System.Drawing.Color]::Black
58
+ $form.Opacity = 0.22
59
+ try { $form.GetType().GetProperty('DoubleBuffered', 'Instance,NonPublic').SetValue($form, $true) } catch {}
60
+
61
+ function Draw-Tag($g, $text, $x, $y) {
62
+ $sz = $g.MeasureString($text, $script:font)
63
+ $w = [int]$sz.Width + 16; $h = [int]$sz.Height + 8
64
+ $bx = [Math]::Max(0, [Math]::Min($x, $script:W - $w))
65
+ $by = [Math]::Max(0, [Math]::Min($y, $script:H - $h))
66
+ $g.FillRectangle($script:brBox, $bx, $by, $w, $h)
67
+ $g.DrawString($text, $script:font, $script:brText, ($bx + 8), ($by + 4))
68
+ }
69
+
70
+ $form.Add_Paint({
71
+ param($s, $e)
72
+ $g = $e.Graphics
73
+ $r = $script:sel
74
+ if ($r -and $r.Width -gt 0 -and $r.Height -gt 0) {
75
+ $g.DrawRectangle($script:pen, $r)
76
+ Draw-Tag $g ("{0} x {1}" -f $r.Width, $r.Height) ($r.Left + 6) ($r.Top + 6)
77
+ }
78
+ else {
79
+ Draw-Tag $g '拖一个框;Esc 取消' 40 40
80
+ }
81
+ })
82
+
83
+ $form.Add_MouseDown({
84
+ param($s, $e)
85
+ if ($e.Button -eq [System.Windows.Forms.MouseButtons]::Left) {
86
+ $script:start = $e.Location
87
+ $script:sel = New-Object System.Drawing.Rectangle $e.X, $e.Y, 0, 0
88
+ $script:dragging = $true
89
+ $s.Invalidate()
90
+ }
91
+ elseif ($e.Button -eq [System.Windows.Forms.MouseButtons]::Right) {
92
+ $script:cancelled = $true
93
+ $s.Close()
94
+ }
95
+ })
96
+
97
+ $form.Add_MouseMove({
98
+ param($s, $e)
99
+ if (-not $script:dragging) { return }
100
+ $x1 = [Math]::Min($script:start.X, $e.X); $y1 = [Math]::Min($script:start.Y, $e.Y)
101
+ $x2 = [Math]::Max($script:start.X, $e.X); $y2 = [Math]::Max($script:start.Y, $e.Y)
102
+ $script:sel = New-Object System.Drawing.Rectangle $x1, $y1, ($x2 - $x1), ($y2 - $y1)
103
+ $s.Invalidate()
104
+ })
105
+
106
+ $closeTimer = New-Object System.Windows.Forms.Timer
107
+ $closeTimer.Interval = 160
108
+ $closeTimer.Add_Tick({ $closeTimer.Stop(); $form.Close() })
109
+
110
+ $form.Add_MouseUp({
111
+ param($s, $e)
112
+ if (-not $script:dragging) { return }
113
+ $script:dragging = $false
114
+ if ($script:sel -and $script:sel.Width -ge 8 -and $script:sel.Height -ge 8) {
115
+ $script:done = $true
116
+ # 先把遮罩藏掉(人立刻看见框定住了),窗口晚 160ms 再关:
117
+ # 松手这一下要是当场关窗,抬键消息会漏给下面的窗口,有的程序会当成一次点击。
118
+ $s.Visible = $false
119
+ $closeTimer.Start()
120
+ }
121
+ })
122
+
123
+ $form.Add_KeyDown({
124
+ param($s, $e)
125
+ if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Escape) {
126
+ $script:cancelled = $true
127
+ $s.Close()
128
+ }
129
+ })
130
+
131
+ $form.Add_Shown({ $form.Activate() })
132
+
133
+ $timer = New-Object System.Windows.Forms.Timer
134
+ $timer.Interval = [Math]::Max(1000, $TimeoutSec * 1000)
135
+ $timer.Add_Tick({ $script:cancelled = $true; $form.Close() })
136
+ $timer.Start()
137
+
138
+ [System.Windows.Forms.Application]::Run($form)
139
+ $timer.Stop()
140
+
141
+ if (-not $script:done -or -not $script:sel) {
142
+ @{ ok = $false; error = 'cancelled' } | ConvertTo-Json -Compress
143
+ exit 1
144
+ }
145
+
146
+ # 等遮罩真的消失,不然 WindowFromPoint 会点到我们自己
147
+ Start-Sleep -Milliseconds 90
148
+
149
+ $sel = $script:sel
150
+ $sx = $vs.Left + $sel.X
151
+ $sy = $vs.Top + $sel.Y
152
+
153
+ $pt = New-Object Rg+POINT
154
+ $pt.X = $sx + [int]($sel.Width / 2)
155
+ $pt.Y = $sy + [int]($sel.Height / 2)
156
+ $h = [Rg]::WindowFromPoint($pt)
157
+ if ($h -eq [IntPtr]::Zero) { @{ ok = $false; error = 'no window under the box' } | ConvertTo-Json -Compress; exit 1 }
158
+ $root = [Rg]::GetAncestor($h, 2)
159
+ if ($root -eq [IntPtr]::Zero) { $root = $h }
160
+
161
+ $sb = New-Object System.Text.StringBuilder 512
162
+ [Rg]::GetWindowText($root, $sb, 512) | Out-Null
163
+ if ($sb.Length -eq 0) {
164
+ @{ ok = $false; error = '那个位置没有窗口(是桌面空白处),换个地方再框' } | ConvertTo-Json -Compress
165
+ exit 1
166
+ }
167
+ $wr = New-Object Rg+RECT
168
+ [Rg]::GetWindowRect($root, [ref]$wr) | Out-Null
169
+ $procId = 0
170
+ [Rg]::GetWindowThreadProcessId($root, [ref]$procId) | Out-Null
171
+
172
+ # 框可能越出窗口:裁到窗口里面,换算成相对窗口左上角
173
+ $ix = [Math]::Max($sx, $wr.Left); $iy = [Math]::Max($sy, $wr.Top)
174
+ $ir = [Math]::Min($sx + $sel.Width, $wr.Right); $ib = [Math]::Min($sy + $sel.Height, $wr.Bottom)
175
+ $cw = [Math]::Max(1, $ir - $ix)
176
+ $ch = [Math]::Max(1, $ib - $iy)
177
+ $cx = $ix - $wr.Left
178
+ $cy = $iy - $wr.Top
179
+
180
+ $res = [ordered]@{
181
+ ok = $true
182
+ when = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')
183
+ hwnd = [int64]$root
184
+ title = $sb.ToString()
185
+ pid = $procId
186
+ rect = @($wr.Left, $wr.Top, ($wr.Right - $wr.Left), ($wr.Bottom - $wr.Top))
187
+ screen = @($sx, $sy, $sel.Width, $sel.Height)
188
+ crop = @($cx, $cy, $cw, $ch)
189
+ }
190
+
191
+ $dir = Split-Path $Out -Parent
192
+ if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
193
+ [System.IO.File]::WriteAllText($Out, ($res | ConvertTo-Json -Compress), (New-Object System.Text.UTF8Encoding($false)))
194
+
195
+ $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