bridgerator 1.0.8 → 1.1.1
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/README.md +176 -110
- package/assets/bridgerator.ico +0 -0
- package/assets/icon-16.png +0 -0
- package/assets/icon-20.png +0 -0
- package/assets/icon-24.png +0 -0
- package/assets/icon-256.png +0 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon-48.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/logo.svg +1 -0
- package/install.ps1 +456 -56
- package/npm-shrinkwrap.json +8621 -0
- package/package.json +55 -49
- package/recipes/backup-folder.json +31 -0
- package/recipes/clean-downloads.json +48 -0
- package/recipes/disk-report.json +66 -0
- package/recipes/sort-folder.json +49 -0
- package/recipes/update-apps.json +25 -0
- package/src/ask-lib.ps1 +230 -0
- package/src/ask.mjs +51 -0
- package/src/ask.ps1 +17 -0
- package/src/automations.mjs +673 -0
- package/src/batch.mjs +221 -0
- package/src/bridge.mjs +720 -29
- package/src/cli.mjs +529 -74
- package/src/confirm.mjs +418 -0
- package/src/dc-config-hooks.mjs +18 -8
- package/src/dc-isolate.mjs +5 -0
- package/src/delegate.mjs +555 -0
- package/src/docs-tool.mjs +494 -0
- package/src/doctor.mjs +200 -0
- package/src/events.mjs +21 -0
- package/src/extra-tools.mjs +124 -0
- package/src/guard.mjs +34 -0
- package/src/notice.mjs +84 -23
- package/src/pair.mjs +27 -4
- package/src/pc-check.mjs +112 -0
- package/src/pc-check.ps1 +103 -0
- package/src/pc-text.mjs +120 -0
- package/src/pdf-font.mjs +245 -0
- package/src/photos-tool.mjs +187 -0
- package/src/photos.ps1 +145 -0
- package/src/plan.mjs +90 -0
- package/src/policy.mjs +293 -124
- package/src/queue.mjs +194 -0
- package/src/recent.mjs +48 -0
- package/src/recipes.mjs +108 -0
- package/src/recycle.mjs +185 -0
- package/src/recycle.ps1 +110 -0
- package/src/screen-tool.mjs +185 -0
- package/src/screen.ps1 +285 -0
- package/src/search-tool.mjs +189 -0
- package/src/search.ps1 +129 -0
- package/src/settings-tool.mjs +186 -0
- package/src/setup-tool.mjs +129 -0
- package/src/software-tool.mjs +185 -0
- package/src/telemetry.mjs +86 -0
- package/src/tool-lib.mjs +177 -0
- package/src/tool-text.mjs +45 -0
- package/src/tray.mjs +97 -0
- package/src/tray.ps1 +359 -0
- package/src/undo.mjs +313 -0
- package/src/win.mjs +17 -0
- package/ui/app.css +388 -0
- package/ui/app.js +868 -0
- package/ui/i18n/en.json +598 -0
- package/ui/i18n/ru.json +598 -0
- package/ui/index.html +26 -0
- package/vendor/webview2/LICENSE.txt +27 -0
- package/vendor/webview2/Microsoft.Web.WebView2.Core.dll +0 -0
- package/vendor/webview2/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/vendor/webview2/NOTICE.txt +85 -0
- package/vendor/webview2/SHA256SUMS.txt +4 -0
- package/vendor/webview2/SOURCE.txt +1 -0
- package/vendor/webview2/arm64/WebView2Loader.dll +0 -0
- package/vendor/webview2/x64/WebView2Loader.dll +0 -0
package/src/recycle.ps1
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Bridgerator: move files / folders to the Recycle Bin (never a permanent delete). Loaded after
|
|
2
|
+
# ask-lib.ps1 (Add-CachedType). Reads a JSON list of absolute paths on stdin, prints one JSON line:
|
|
3
|
+
# [{p, r}] with r = 'recycled' | 'nobin' (this drive has no Recycle Bin / the item is too big for it:
|
|
4
|
+
# nothing was deleted) | 'missing' | 'error:<hresult>'; a recycled item also carries bin = the path of
|
|
5
|
+
# its copy in the Recycle Bin ($R...), so Undo later removes exactly that copy and no other.
|
|
6
|
+
# IFileOperation with FOFX_RECYCLEONDELETE and no UI; a progress sink checks PreDeleteItem: without
|
|
7
|
+
# TSF_DELETE_RECYCLE_IF_POSSIBLE the shell would delete for good, so the item is aborted instead.
|
|
8
|
+
$ErrorActionPreference = 'Stop'
|
|
9
|
+
[Console]::InputEncoding = [Text.UTF8Encoding]::new($false)
|
|
10
|
+
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
|
|
11
|
+
Add-CachedType @'
|
|
12
|
+
using System; using System.IO; using System.Runtime.InteropServices;
|
|
13
|
+
[ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
14
|
+
public interface IBrShellItem { void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv); void GetParent(out IBrShellItem ppsi); void GetDisplayName(uint sigdn, out IntPtr name); void GetAttributes(uint mask, out uint attrs); void Compare(IBrShellItem psi, uint hint, out int order); }
|
|
15
|
+
[ComImport, Guid("04b0f1a7-9490-44bc-96e1-4296a31252e2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
16
|
+
public interface IBrSink {
|
|
17
|
+
[PreserveSig] int StartOperations();
|
|
18
|
+
[PreserveSig] int FinishOperations(int hr);
|
|
19
|
+
[PreserveSig] int PreRenameItem(uint f, IBrShellItem i, [MarshalAs(UnmanagedType.LPWStr)] string n);
|
|
20
|
+
[PreserveSig] int PostRenameItem(uint f, IBrShellItem i, [MarshalAs(UnmanagedType.LPWStr)] string n, int hr, IBrShellItem c);
|
|
21
|
+
[PreserveSig] int PreMoveItem(uint f, IBrShellItem i, IBrShellItem d, [MarshalAs(UnmanagedType.LPWStr)] string n);
|
|
22
|
+
[PreserveSig] int PostMoveItem(uint f, IBrShellItem i, IBrShellItem d, [MarshalAs(UnmanagedType.LPWStr)] string n, int hr, IBrShellItem c);
|
|
23
|
+
[PreserveSig] int PreCopyItem(uint f, IBrShellItem i, IBrShellItem d, [MarshalAs(UnmanagedType.LPWStr)] string n);
|
|
24
|
+
[PreserveSig] int PostCopyItem(uint f, IBrShellItem i, IBrShellItem d, [MarshalAs(UnmanagedType.LPWStr)] string n, int hr, IBrShellItem c);
|
|
25
|
+
[PreserveSig] int PreDeleteItem(uint f, IBrShellItem i);
|
|
26
|
+
[PreserveSig] int PostDeleteItem(uint f, IBrShellItem i, int hr, IBrShellItem c);
|
|
27
|
+
[PreserveSig] int PreNewItem(uint f, IBrShellItem d, [MarshalAs(UnmanagedType.LPWStr)] string n);
|
|
28
|
+
[PreserveSig] int PostNewItem(uint f, IBrShellItem d, [MarshalAs(UnmanagedType.LPWStr)] string n, [MarshalAs(UnmanagedType.LPWStr)] string t, uint a, int hr, IBrShellItem c);
|
|
29
|
+
[PreserveSig] int UpdateProgress(uint total, uint sofar);
|
|
30
|
+
[PreserveSig] int ResetTimer();
|
|
31
|
+
[PreserveSig] int PauseTimer();
|
|
32
|
+
[PreserveSig] int ResumeTimer();
|
|
33
|
+
}
|
|
34
|
+
[ComImport, Guid("947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
35
|
+
public interface IBrFileOperation {
|
|
36
|
+
uint Advise(IBrSink s); void Unadvise(uint c); void SetOperationFlags(uint f);
|
|
37
|
+
void SetProgressMessage([MarshalAs(UnmanagedType.LPWStr)] string m); void SetProgressDialog([MarshalAs(UnmanagedType.IUnknown)] object d);
|
|
38
|
+
void SetProperties([MarshalAs(UnmanagedType.IUnknown)] object p); void SetOwnerWindow(IntPtr h);
|
|
39
|
+
void ApplyPropertiesToItem(IBrShellItem i); void ApplyPropertiesToItems([MarshalAs(UnmanagedType.IUnknown)] object i);
|
|
40
|
+
void RenameItem(IBrShellItem i, [MarshalAs(UnmanagedType.LPWStr)] string n, IBrSink s); void RenameItems([MarshalAs(UnmanagedType.IUnknown)] object i, [MarshalAs(UnmanagedType.LPWStr)] string n);
|
|
41
|
+
void MoveItem(IBrShellItem i, IBrShellItem d, [MarshalAs(UnmanagedType.LPWStr)] string n, IBrSink s); void MoveItems([MarshalAs(UnmanagedType.IUnknown)] object i, IBrShellItem d);
|
|
42
|
+
void CopyItem(IBrShellItem i, IBrShellItem d, [MarshalAs(UnmanagedType.LPWStr)] string n, IBrSink s); void CopyItems([MarshalAs(UnmanagedType.IUnknown)] object i, IBrShellItem d);
|
|
43
|
+
void DeleteItem(IBrShellItem i, IBrSink s); void DeleteItems([MarshalAs(UnmanagedType.IUnknown)] object i);
|
|
44
|
+
void NewItem(IBrShellItem d, uint a, [MarshalAs(UnmanagedType.LPWStr)] string n, [MarshalAs(UnmanagedType.LPWStr)] string t, IBrSink s);
|
|
45
|
+
void PerformOperations(); [return: MarshalAs(UnmanagedType.Bool)] bool GetAnyOperationsAborted();
|
|
46
|
+
}
|
|
47
|
+
public class BrRecycleSink : IBrSink {
|
|
48
|
+
public bool Seen, ToBin; public int Hr; public string Bin;
|
|
49
|
+
public int PreDeleteItem(uint f, IBrShellItem i) { Seen = true; ToBin = (f & 0x80) != 0; return ToBin ? 0 : unchecked((int)0x80004004); }
|
|
50
|
+
public int PostDeleteItem(uint f, IBrShellItem i, int hr, IBrShellItem c) {
|
|
51
|
+
Hr = hr;
|
|
52
|
+
// c = the item now in the Recycle Bin (null when deleted for good): its file-system path (SIGDN_FILESYSPATH).
|
|
53
|
+
if (c != null) { try { IntPtr n; c.GetDisplayName(0x80058000, out n); Bin = Marshal.PtrToStringUni(n); Marshal.FreeCoTaskMem(n); } catch { Bin = null; } }
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
public int StartOperations() { return 0; } public int FinishOperations(int hr) { return 0; }
|
|
57
|
+
public int PreRenameItem(uint f, IBrShellItem i, string n) { return 0; } public int PostRenameItem(uint f, IBrShellItem i, string n, int hr, IBrShellItem c) { return 0; }
|
|
58
|
+
public int PreMoveItem(uint f, IBrShellItem i, IBrShellItem d, string n) { return 0; } public int PostMoveItem(uint f, IBrShellItem i, IBrShellItem d, string n, int hr, IBrShellItem c) { return 0; }
|
|
59
|
+
public int PreCopyItem(uint f, IBrShellItem i, IBrShellItem d, string n) { return 0; } public int PostCopyItem(uint f, IBrShellItem i, IBrShellItem d, string n, int hr, IBrShellItem c) { return 0; }
|
|
60
|
+
public int PreNewItem(uint f, IBrShellItem d, string n) { return 0; } public int PostNewItem(uint f, IBrShellItem d, string n, string t, uint a, int hr, IBrShellItem c) { return 0; }
|
|
61
|
+
public int UpdateProgress(uint t, uint s) { return 0; } public int ResetTimer() { return 0; } public int PauseTimer() { return 0; } public int ResumeTimer() { return 0; }
|
|
62
|
+
}
|
|
63
|
+
public static class BrRecycle {
|
|
64
|
+
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
|
|
65
|
+
static extern void SHCreateItemFromParsingName(string path, IntPtr pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, [MarshalAs(UnmanagedType.Interface)] out IBrShellItem item);
|
|
66
|
+
// FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOCONFIRMMKDIR | FOF_ALLOWUNDO | FOFX_RECYCLEONDELETE | FOFX_EARLYFAILURE
|
|
67
|
+
const uint FLAGS = 0x0004 | 0x0010 | 0x0400 | 0x0200 | 0x0040 | 0x00080000 | 0x00100000;
|
|
68
|
+
public static string LastBin;
|
|
69
|
+
public static string One(string path) {
|
|
70
|
+
LastBin = null;
|
|
71
|
+
if (!File.Exists(path) && !Directory.Exists(path)) return "missing";
|
|
72
|
+
try {
|
|
73
|
+
var root = Path.GetPathRoot(path);
|
|
74
|
+
if (string.IsNullOrEmpty(root) || root.StartsWith(@"\\")) return "nobin";
|
|
75
|
+
var dt = new DriveInfo(root).DriveType;
|
|
76
|
+
if (dt != DriveType.Fixed && dt != DriveType.Removable) return "nobin";
|
|
77
|
+
} catch { return "nobin"; }
|
|
78
|
+
var op = (IBrFileOperation)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("3ad05575-8857-4850-9277-11b85bdb8e09")));
|
|
79
|
+
var sink = new BrRecycleSink();
|
|
80
|
+
try {
|
|
81
|
+
op.SetOperationFlags(FLAGS);
|
|
82
|
+
IBrShellItem item; SHCreateItemFromParsingName(path, IntPtr.Zero, typeof(IBrShellItem).GUID, out item);
|
|
83
|
+
op.DeleteItem(item, sink);
|
|
84
|
+
try { op.PerformOperations(); } catch (COMException e) { if (sink.Seen && !sink.ToBin) return "nobin"; return "error:" + e.ErrorCode.ToString("x8"); }
|
|
85
|
+
if (sink.Seen && !sink.ToBin) return "nobin";
|
|
86
|
+
if (sink.Hr < 0) return "error:" + sink.Hr.ToString("x8");
|
|
87
|
+
if (!sink.Seen || File.Exists(path) || Directory.Exists(path)) return op.GetAnyOperationsAborted() ? "nobin" : "error:not-deleted";
|
|
88
|
+
LastBin = sink.Bin;
|
|
89
|
+
return "recycled";
|
|
90
|
+
} catch (Exception e) { return "error:" + e.GetType().Name; }
|
|
91
|
+
finally { Marshal.ReleaseComObject(op); }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
'@
|
|
95
|
+
$list = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
96
|
+
# Each item: a path, or {p, skip}: skip = letters h / s / r - a hidden / system / read-only item is left
|
|
97
|
+
# alone ('skipped'), as the command itself would (a wildcard without -Force / /a / /f).
|
|
98
|
+
function Test-Skip([string]$path, [string]$skip) {
|
|
99
|
+
if (-not $skip) { return $false }
|
|
100
|
+
try { $a = [IO.File]::GetAttributes($path) } catch { return $false }
|
|
101
|
+
return (($skip.Contains('h') -and ($a -band [IO.FileAttributes]::Hidden)) -or ($skip.Contains('s') -and ($a -band [IO.FileAttributes]::System)) -or ($skip.Contains('r') -and ($a -band [IO.FileAttributes]::ReadOnly)))
|
|
102
|
+
}
|
|
103
|
+
$out = @(foreach ($x in @($list)) {
|
|
104
|
+
if ($x -is [string]) { $p = $x; $skip = '' } else { $p = [string]$x.p; $skip = [string]$x.skip }
|
|
105
|
+
if (Test-Skip $p $skip) { @{ p = $p; r = 'skipped' } } else {
|
|
106
|
+
$r = [BrRecycle]::One($p)
|
|
107
|
+
if ($r -eq 'recycled' -and [BrRecycle]::LastBin) { @{ p = $p; r = $r; bin = [BrRecycle]::LastBin } } else { @{ p = $p; r = $r } }
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
[Console]::Out.WriteLine((ConvertTo-Json -InputObject @($out) -Compress -Depth 3))
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// LOGIC: see critical-logic/bridge.md ("Extra tools", screen). `screen`: the list of open windows, a
|
|
2
|
+
// screenshot of the whole screen / one monitor / one window, and text recognition (Windows' built-in
|
|
3
|
+
// OCR, the languages installed on the PC) of the screen, a window or an image file. Everything runs in
|
|
4
|
+
// screen.ps1 (user32 + System.Drawing + Windows.Media.Ocr); no extra programs, nothing downloaded.
|
|
5
|
+
// Every screenshot shows a notification on the PC ("the AI took a screenshot") unless the person turned
|
|
6
|
+
// that off (screen.notify). Screenshots can be switched off completely (screen.enabled).
|
|
7
|
+
// A locked screen / sign-in screen / UAC prompt can't be captured: an honest error, never a black picture.
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { showNotice } from './notice.mjs';
|
|
12
|
+
import { fail, guard, freeName, needAction, str, int, psJson, ToolError } from './tool-lib.mjs';
|
|
13
|
+
|
|
14
|
+
const PS = fileURLToPath(new URL('./screen.ps1', import.meta.url));
|
|
15
|
+
export const MAX_SIDE = 1600; // long side of the picture sent to the AI
|
|
16
|
+
export const MAX_BYTES = 900 * 1024; // JPEG bytes (base64 ≈ 1.2 MB)
|
|
17
|
+
const MAX_B64 = Math.ceil(MAX_BYTES / 3) * 4 + 4;
|
|
18
|
+
const MAX_TEXT = 60000;
|
|
19
|
+
const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tif', '.tiff']);
|
|
20
|
+
const ACTIONS = ['windows', 'capture', 'ocr'];
|
|
21
|
+
|
|
22
|
+
export const defs = [{
|
|
23
|
+
name: 'screen',
|
|
24
|
+
title: 'Screen and text recognition',
|
|
25
|
+
description: 'Screenshots and text recognition on this PC. action "windows": open windows (id, title, program). '
|
|
26
|
+
+ '"capture": a picture of the whole screen, one monitor (monitor) or one window (window = part of its title, or window_id); save_to also keeps a full-size copy. '
|
|
27
|
+
+ '"ocr": the text on the screen, a window or an image file (path), recognized by Windows (languages installed on the PC; language like "en-US"). '
|
|
28
|
+
+ 'The person sees a notification on the PC for each screenshot. A locked screen can\'t be captured.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object', additionalProperties: false, required: ['action'],
|
|
31
|
+
properties: {
|
|
32
|
+
action: { type: 'string', enum: ACTIONS },
|
|
33
|
+
window: { type: 'string', maxLength: 200, description: 'Part of the window title (any case)' },
|
|
34
|
+
window_id: { type: 'integer', minimum: 1, description: 'Window id from action "windows"' },
|
|
35
|
+
monitor: { type: 'integer', minimum: 1, maximum: 16, description: 'Monitor number; without it: all monitors' },
|
|
36
|
+
language: { type: 'string', maxLength: 20, description: 'OCR language tag, e.g. "en-US", "ru"' },
|
|
37
|
+
path: { type: 'string', maxLength: 1000, description: 'Image file for ocr instead of the screen' },
|
|
38
|
+
save_to: { type: 'string', maxLength: 1000, description: 'Full path of a .png or .jpg for a full-size copy (capture)' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
42
|
+
}];
|
|
43
|
+
|
|
44
|
+
export const settings = [
|
|
45
|
+
{ key: 'screen.enabled', default: true, widensWhen: true, label: { en: 'Screenshots and screen text recognition', ru: 'Снимки экрана и распознавание текста' }, hint: { en: 'The AI can take a picture of the screen or a window.', ru: 'ИИ может сделать снимок экрана или окна.' } },
|
|
46
|
+
{ key: 'screen.notify', default: true, widensWhen: false, label: { en: 'Notify me about every screenshot', ru: 'Уведомлять о каждом снимке экрана' }, hint: { en: 'A notification "the AI took a screenshot" appears on the PC.', ru: 'На ПК появляется уведомление «ИИ сделал снимок экрана».' } },
|
|
47
|
+
];
|
|
48
|
+
const DEFAULT = Object.fromEntries(settings.map((s) => [s.key, s.default]));
|
|
49
|
+
function setting(ctx, key) {
|
|
50
|
+
if (ctx.extra?.value) return ctx.extra.value(ctx.cfg, key);
|
|
51
|
+
const [g, k] = key.split('.');
|
|
52
|
+
const v = ctx.cfg?.[g]?.[k];
|
|
53
|
+
return typeof v === 'boolean' ? v : DEFAULT[key];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const clean = (s, n = 200) => String(s ?? '').replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, ' ').trim().slice(0, n);
|
|
57
|
+
const run = (ctx, env, timeout = 60000) => psJson(ctx, { files: [PS], env, timeout });
|
|
58
|
+
const check = (r) => { if (!r || r.ok !== true) throw new ToolError(r?.code || 'PS_FAILED', MESSAGES[r?.code] || clean(r?.message, 300) || 'Windows could not do it.', HINTS[r?.code] || ''); return r; };
|
|
59
|
+
const MESSAGES = {
|
|
60
|
+
SCREEN_LOCKED: 'The screen is locked (or the sign-in screen / a Windows security prompt is open), so no screenshot can be taken until the person unlocks the PC.',
|
|
61
|
+
};
|
|
62
|
+
const HINTS = {
|
|
63
|
+
SCREEN_LOCKED: 'Ask the person to unlock the PC and try again.',
|
|
64
|
+
NO_WINDOW: 'Call screen with action "windows" to see the open windows.',
|
|
65
|
+
NO_MONITOR: 'Leave monitor out to capture all monitors.',
|
|
66
|
+
NO_OCR_LANG: 'The person can add a language with text recognition in Windows Settings → Time & language → Language & region (Add a language).',
|
|
67
|
+
BAD_LANG: 'Use one of the installed languages, or leave language out.',
|
|
68
|
+
BAD_IMAGE: 'Use a PNG, JPEG, BMP, GIF or TIFF file.',
|
|
69
|
+
IMAGE_TOO_LARGE: 'Use a smaller image or capture one window instead of all monitors.',
|
|
70
|
+
EXISTS: 'Call again: a new free name is chosen.',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Part of a title → one window: exact title first, then the topmost window whose title contains it. */
|
|
74
|
+
export function pickWindow(list, query) {
|
|
75
|
+
const q = clean(query).toLowerCase();
|
|
76
|
+
if (!q) fail('BAD_ARGS', 'window must name a part of a window title.');
|
|
77
|
+
const hits = list.filter((w) => String(w.title).toLowerCase().includes(q));
|
|
78
|
+
if (!hits.length) {
|
|
79
|
+
const titles = list.slice(0, 15).map((w) => `"${clean(w.title, 80)}"`).join(', ');
|
|
80
|
+
fail('NO_WINDOW', `No open window has "${clean(query, 80)}" in its title.${titles ? ` Open windows: ${titles}.` : ''}`, HINTS.NO_WINDOW);
|
|
81
|
+
}
|
|
82
|
+
const exact = hits.find((w) => String(w.title).toLowerCase() === q);
|
|
83
|
+
return { win: exact || hits[0], others: hits.length - 1 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const kb = (n) => `${Math.max(1, Math.round(n / 1024))} KB`;
|
|
87
|
+
|
|
88
|
+
// The capture runs in a second PowerShell: it checks the id still belongs to the same program.
|
|
89
|
+
const winEnv = (win) => ({ BRX_HWND: String(win.id), ...(Number.isInteger(win.pid) && win.pid > 0 ? { BRX_PID: String(win.pid) } : {}) });
|
|
90
|
+
async function target(ctx, args) {
|
|
91
|
+
const windowQuery = str(args, 'window', { max: 200 });
|
|
92
|
+
const windowId = args.window_id === undefined ? undefined : int(args, 'window_id', { min: 1, max: Number.MAX_SAFE_INTEGER });
|
|
93
|
+
const monitor = int(args, 'monitor', { min: 1, max: 16, def: 0 });
|
|
94
|
+
if ((windowQuery || windowId) && monitor) fail('BAD_ARGS', 'Use either window / window_id or monitor, not both.');
|
|
95
|
+
if (windowId || windowQuery) {
|
|
96
|
+
const r = check(await run(ctx, { BRX_ACTION: 'windows' }));
|
|
97
|
+
const list = Array.isArray(r.windows) ? r.windows : [];
|
|
98
|
+
if (windowId) {
|
|
99
|
+
const win = list.find((w) => w.id === windowId);
|
|
100
|
+
if (!win) fail('NO_WINDOW', 'That window is not open any more.', HINTS.NO_WINDOW);
|
|
101
|
+
return { env: winEnv(win), what: 'window', win, others: 0 };
|
|
102
|
+
}
|
|
103
|
+
const { win, others } = pickWindow(list, windowQuery);
|
|
104
|
+
return { env: winEnv(win), what: 'window', win, others };
|
|
105
|
+
}
|
|
106
|
+
return { env: monitor ? { BRX_MONITOR: String(monitor) } : {}, what: monitor ? 'monitor' : 'screen', monitor };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function saveTarget(ctx, p) {
|
|
110
|
+
if (p === undefined) return undefined;
|
|
111
|
+
const abs = guard(ctx, p, { write: true });
|
|
112
|
+
if (!['.png', '.jpg', '.jpeg'].includes(path.extname(abs).toLowerCase())) fail('BAD_PATH', 'save_to must end with .png or .jpg.');
|
|
113
|
+
if (!fs.existsSync(path.dirname(abs))) fail('BAD_PATH', `The folder "${path.dirname(abs)}" does not exist.`, 'Create the folder first or choose another one.');
|
|
114
|
+
return freeName(abs);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function indicate(ctx, t, ocr) {
|
|
118
|
+
if (!setting(ctx, 'screen.notify')) return;
|
|
119
|
+
try {
|
|
120
|
+
(ctx.notice ?? showNotice)({ kind: 'screenshot', what: t.what, monitor: t.monitor, title: t.win?.title, ocr }, ctx.log || (() => {}));
|
|
121
|
+
} catch (e) { ctx.log?.(`[bridgerator] screenshot notice failed: ${e?.message || e}`); }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const describeTarget = (t) => (t.what === 'window' ? `window "${clean(t.win.title, 120)}"` : t.what === 'monitor' ? `monitor ${t.monitor}` : 'the whole screen');
|
|
125
|
+
|
|
126
|
+
function linesText(r) {
|
|
127
|
+
let s = (Array.isArray(r.lines) ? r.lines : []).map((l) => clean(l, 2000)).join('\n');
|
|
128
|
+
if (s.length > MAX_TEXT) s = `${s.slice(0, MAX_TEXT)}\n[… cut at ${MAX_TEXT} characters]`;
|
|
129
|
+
return s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function handle(name, args, ctx) {
|
|
133
|
+
const action = needAction(args, ACTIONS);
|
|
134
|
+
const lang = str(args, 'language', { max: 20 });
|
|
135
|
+
if (lang && !/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i.test(lang)) fail('BAD_ARGS', 'language must be a tag like "en-US" or "ru".');
|
|
136
|
+
|
|
137
|
+
// OCR of an image file is not a screenshot: no screen setting, no notice — the folder rules apply.
|
|
138
|
+
if (action === 'ocr' && args.path !== undefined) {
|
|
139
|
+
const file = guard(ctx, str(args, 'path', { required: true, max: 1000 }));
|
|
140
|
+
if (!IMAGE_EXT.has(path.extname(file).toLowerCase())) fail('BAD_IMAGE', 'This is not an image file.', HINTS.BAD_IMAGE);
|
|
141
|
+
const st = fs.statSync(file);
|
|
142
|
+
if (!st.isFile()) fail('BAD_PATH', 'path must be an image file, not a folder.');
|
|
143
|
+
if (st.size > 50 * 1024 * 1024) fail('IMAGE_TOO_LARGE', 'The image is larger than 50 MB.', HINTS.IMAGE_TOO_LARGE);
|
|
144
|
+
const r = check(await run(ctx, { BRX_ACTION: 'ocr', BRX_FILE: file, ...(lang ? { BRX_LANG: lang } : {}) }, 120000));
|
|
145
|
+
const body = linesText(r);
|
|
146
|
+
return { content: [{ type: 'text', text: `Text recognized in ${path.basename(file)} (${r.width}×${r.height}, language ${clean(r.language, 20)}):\n${body || '[no text found]'}` }] };
|
|
147
|
+
}
|
|
148
|
+
if (args.path !== undefined) fail('BAD_ARGS', 'path is only for action "ocr".');
|
|
149
|
+
|
|
150
|
+
if (!setting(ctx, 'screen.enabled')) {
|
|
151
|
+
fail('SCREEN_OFF', 'Screenshots are turned off on this PC.', 'The person can turn them on in the Bridgerator window (Settings), or you can ask with bridgerator_settings (screen.enabled); the person confirms it on the PC.');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (action === 'windows') {
|
|
155
|
+
const r = check(await run(ctx, { BRX_ACTION: 'windows' }));
|
|
156
|
+
const windows = (Array.isArray(r.windows) ? r.windows : []).slice(0, 100).map((w) => ({ id: w.id, title: clean(w.title), program: clean(w.process, 60), ...(w.minimized ? { minimized: true } : {}) }));
|
|
157
|
+
return { content: [{ type: 'text', text: JSON.stringify({ windows }, null, 2) }] };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (action === 'ocr' && args.save_to !== undefined) fail('BAD_ARGS', 'save_to is only for action "capture".');
|
|
161
|
+
const t = await target(ctx, args);
|
|
162
|
+
const save = action === 'capture' ? saveTarget(ctx, args.save_to) : undefined;
|
|
163
|
+
const env = { BRX_ACTION: action, ...t.env, ...(lang ? { BRX_LANG: lang } : {}), ...(save ? { BRX_SAVE: save } : {}),
|
|
164
|
+
...(action === 'capture' ? { BRX_IMAGE: '1', BRX_MAXSIDE: String(MAX_SIDE), BRX_MAXBYTES: String(MAX_BYTES) } : {}) };
|
|
165
|
+
// The person is told about every picture taken (after the capture, so the notice is not in it) — also
|
|
166
|
+
// when a later step failed (taken) or the run did not answer (it may have taken one).
|
|
167
|
+
let raw;
|
|
168
|
+
try { raw = await run(ctx, env, action === 'ocr' ? 120000 : 60000); } catch (e) { indicate(ctx, t, action === 'ocr'); throw e; }
|
|
169
|
+
if (raw?.ok === true || raw?.taken === true) indicate(ctx, t, action === 'ocr');
|
|
170
|
+
const r = check(raw);
|
|
171
|
+
const notes = [
|
|
172
|
+
...(t.others ? [`${t.others} other window(s) also match "${clean(args.window, 80)}"; use window_id to pick another one.`] : []),
|
|
173
|
+
...(r.minimized ? ['The window is minimized: Windows may give an empty or old picture of it.'] : []),
|
|
174
|
+
];
|
|
175
|
+
if (action === 'ocr') {
|
|
176
|
+
const body = linesText(r);
|
|
177
|
+
return { content: [{ type: 'text', text: `Text on ${describeTarget(t)} (${r.width}×${r.height}, language ${clean(r.language, 20)}):\n${body || '[no text found]'}${notes.length ? `\n\n${notes.join(' ')}` : ''}` }] };
|
|
178
|
+
}
|
|
179
|
+
if (typeof r.data !== 'string' || !r.data || r.data.length > MAX_B64 || !/^[A-Za-z0-9+/]+=*$/.test(r.data.slice(-64))) {
|
|
180
|
+
fail('IMAGE_TOO_LARGE', 'The screenshot could not be made small enough to send.', HINTS.IMAGE_TOO_LARGE);
|
|
181
|
+
}
|
|
182
|
+
const head = `Screenshot of ${describeTarget(t)}: ${r.width}×${r.height}, sent as ${r.outWidth}×${r.outHeight} JPEG (${kb(r.bytes || r.data.length * 0.75)}).`;
|
|
183
|
+
const saved = r.saved ? ` Full-size copy saved: ${r.saved}` : '';
|
|
184
|
+
return { content: [{ type: 'text', text: `${head}${saved}${notes.length ? ` ${notes.join(' ')}` : ''}` }, { type: 'image', data: r.data, mimeType: 'image/jpeg' }] };
|
|
185
|
+
}
|
package/src/screen.ps1
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# LOGIC: see critical-logic/bridge.md ("Extra tools", screen). Screenshot / window list / OCR for the
|
|
2
|
+
# `screen` tool (screen-tool.mjs). Windows PowerShell 5.1, no extra programs: user32 + System.Drawing for
|
|
3
|
+
# the picture, the built-in Windows.Media.Ocr engine for text. Input only through BRX_* environment
|
|
4
|
+
# variables; output = ONE JSON line. Never shows a window, never moves or restores the person's windows.
|
|
5
|
+
# BRX_ACTION windows | capture | ocr | langs
|
|
6
|
+
# BRX_HWND window id (capture / ocr of one window; BRX_PID = its process then, must still match), BRX_MONITOR 1-based monitor (0 = all)
|
|
7
|
+
# BRX_FILE image file to recognize (ocr), BRX_LANG OCR language tag (empty = the user's languages)
|
|
8
|
+
# BRX_IMAGE 1 = return the compressed picture, BRX_MAXSIDE / BRX_MAXBYTES its limits, BRX_SAVE full-size copy path
|
|
9
|
+
|
|
10
|
+
function Out-Json($o) { [Console]::Out.WriteLine((ConvertTo-Json -InputObject $o -Depth 6 -Compress)) }
|
|
11
|
+
# taken = a picture was already made: the bridge shows the screenshot notice even when a later step fails.
|
|
12
|
+
$script:taken = $false
|
|
13
|
+
function Fail([string]$code, [string]$message, $extra = @{}) { $o = @{ ok = $false; code = $code; message = $message }; if ($script:taken) { $o.taken = $true }; foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }; Out-Json $o; exit 0 }
|
|
14
|
+
|
|
15
|
+
# Compiled helpers are cached like the tray helper's (%LOCALAPPDATA%\Bridgerator\cache, br-<hash>.dll).
|
|
16
|
+
function Add-CachedType([string]$code, [string[]]$refs = @()) {
|
|
17
|
+
try {
|
|
18
|
+
$dir = Join-Path $env:LOCALAPPDATA 'Bridgerator\cache'
|
|
19
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes("$code|$($refs -join ',')|$($PSVersionTable.CLRVersion)")
|
|
20
|
+
$key = -join ([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)[0..11] | ForEach-Object { $_.ToString('x2') })
|
|
21
|
+
$dll = Join-Path $dir "br-$key.dll"
|
|
22
|
+
if (-not [IO.File]::Exists($dll)) {
|
|
23
|
+
[void][IO.Directory]::CreateDirectory($dir)
|
|
24
|
+
$tmp = Join-Path $dir "br-$key.$PID.dll"
|
|
25
|
+
Add-Type -TypeDefinition $code -ReferencedAssemblies $refs -OutputAssembly $tmp -OutputType Library
|
|
26
|
+
try { [IO.File]::Move($tmp, $dll) } catch {
|
|
27
|
+
# Another run compiled the same helper at the same moment: use its file, drop ours.
|
|
28
|
+
if ([IO.File]::Exists($dll)) { try { [IO.File]::Delete($tmp) } catch { } } else { $dll = $tmp }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
Add-Type -Path $dll
|
|
32
|
+
} catch {
|
|
33
|
+
Add-Type -TypeDefinition $code -ReferencedAssemblies $refs
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Add-CachedType -refs System.Drawing @'
|
|
38
|
+
using System; using System.Text; using System.Collections.Generic; using System.Runtime.InteropServices;
|
|
39
|
+
using System.Drawing; using System.Drawing.Imaging; using System.Drawing.Drawing2D; using System.IO;
|
|
40
|
+
public static class BrShot {
|
|
41
|
+
delegate bool EnumProc(IntPtr h, IntPtr l);
|
|
42
|
+
delegate bool MonProc(IntPtr m, IntPtr dc, ref RECT r, IntPtr l);
|
|
43
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }
|
|
44
|
+
[DllImport("user32.dll")] static extern bool EnumWindows(EnumProc f, IntPtr l);
|
|
45
|
+
[DllImport("user32.dll")] static extern bool EnumDisplayMonitors(IntPtr dc, IntPtr clip, MonProc f, IntPtr l);
|
|
46
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
|
|
47
|
+
[DllImport("user32.dll")] static extern bool IsWindow(IntPtr h);
|
|
48
|
+
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr h);
|
|
49
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
|
|
50
|
+
[DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
|
|
51
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
52
|
+
[DllImport("user32.dll")] static extern int GetWindowLong(IntPtr h, int idx);
|
|
53
|
+
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
54
|
+
[DllImport("user32.dll")] static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
|
|
55
|
+
[DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int a, out int v, int size);
|
|
56
|
+
[DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int a, out RECT r, int size);
|
|
57
|
+
[DllImport("user32.dll")] static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access);
|
|
58
|
+
[DllImport("user32.dll")] static extern bool CloseDesktop(IntPtr h);
|
|
59
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool GetUserObjectInformation(IntPtr h, int idx, StringBuilder buf, int len, out int needed);
|
|
60
|
+
[DllImport("user32.dll")] static extern IntPtr SetThreadDpiAwarenessContext(IntPtr c);
|
|
61
|
+
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
|
|
62
|
+
|
|
63
|
+
// Physical pixels on every monitor (per-monitor v2; older Windows: system aware).
|
|
64
|
+
public static void Dpi() {
|
|
65
|
+
try { if (SetThreadDpiAwarenessContext(new IntPtr(-4)) != IntPtr.Zero) return; } catch (Exception) { }
|
|
66
|
+
try { SetProcessDPIAware(); } catch (Exception) { }
|
|
67
|
+
}
|
|
68
|
+
// The desktop that gets the keyboard: "Default" when the person is working; locked screen, sign-in
|
|
69
|
+
// screen and UAC prompts are on the secure desktop (not readable from here) -> null.
|
|
70
|
+
public static string InputDesktop() {
|
|
71
|
+
IntPtr d = OpenInputDesktop(0, false, 0x0001);
|
|
72
|
+
if (d == IntPtr.Zero) return null;
|
|
73
|
+
try { StringBuilder b = new StringBuilder(256); int n; return GetUserObjectInformation(d, 2, b, b.Capacity * 2, out n) ? b.ToString() : null; }
|
|
74
|
+
finally { CloseDesktop(d); }
|
|
75
|
+
}
|
|
76
|
+
public class Win { public long Id; public string Title; public uint Pid; public bool Minimized; public int W, H; }
|
|
77
|
+
public static List<Win> Windows() {
|
|
78
|
+
List<Win> list = new List<Win>();
|
|
79
|
+
EnumWindows(delegate(IntPtr h, IntPtr l) {
|
|
80
|
+
if (!IsWindowVisible(h)) return true;
|
|
81
|
+
int len = GetWindowTextLength(h);
|
|
82
|
+
if (len <= 0) return true;
|
|
83
|
+
if ((GetWindowLong(h, -20) & 0x80) != 0) return true; // WS_EX_TOOLWINDOW
|
|
84
|
+
int cloaked = 0;
|
|
85
|
+
if (DwmGetWindowAttribute(h, 14, out cloaked, 4) == 0 && cloaked != 0) return true; // hidden store apps
|
|
86
|
+
StringBuilder sb = new StringBuilder(len + 1); GetWindowText(h, sb, sb.Capacity);
|
|
87
|
+
RECT r; GetWindowRect(h, out r);
|
|
88
|
+
uint pid; GetWindowThreadProcessId(h, out pid);
|
|
89
|
+
Win w = new Win(); w.Id = h.ToInt64(); w.Title = sb.ToString(); w.Pid = pid; w.Minimized = IsIconic(h); w.W = r.R - r.L; w.H = r.B - r.T;
|
|
90
|
+
if (!w.Minimized && (w.W <= 1 || w.H <= 1)) return true;
|
|
91
|
+
list.Add(w);
|
|
92
|
+
return true;
|
|
93
|
+
}, IntPtr.Zero);
|
|
94
|
+
return list;
|
|
95
|
+
}
|
|
96
|
+
public static Rectangle[] Monitors() {
|
|
97
|
+
List<Rectangle> m = new List<Rectangle>();
|
|
98
|
+
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate(IntPtr hm, IntPtr dc, ref RECT r, IntPtr l) { m.Add(Rectangle.FromLTRB(r.L, r.T, r.R, r.B)); return true; }, IntPtr.Zero);
|
|
99
|
+
return m.ToArray();
|
|
100
|
+
}
|
|
101
|
+
public static Bitmap Screen(int monitor) {
|
|
102
|
+
Rectangle[] mons = Monitors();
|
|
103
|
+
Rectangle b;
|
|
104
|
+
if (monitor > 0) { if (monitor > mons.Length) return null; b = mons[monitor - 1]; }
|
|
105
|
+
else { b = mons.Length > 0 ? mons[0] : Rectangle.Empty; foreach (Rectangle x in mons) b = Rectangle.Union(b, x); }
|
|
106
|
+
if (b.Width <= 0 || b.Height <= 0) return null;
|
|
107
|
+
Bitmap bmp = new Bitmap(b.Width, b.Height, PixelFormat.Format32bppArgb);
|
|
108
|
+
using (Graphics g = Graphics.FromImage(bmp)) { g.Clear(Color.Black); g.CopyFromScreen(b.Left, b.Top, 0, 0, b.Size, CopyPixelOperation.SourceCopy); }
|
|
109
|
+
return bmp;
|
|
110
|
+
}
|
|
111
|
+
public static bool IsMinimized(long id) { return IsIconic(new IntPtr(id)); }
|
|
112
|
+
public static uint Owner(long id) { uint pid; GetWindowThreadProcessId(new IntPtr(id), out pid); return pid; }
|
|
113
|
+
public static bool Exists(long id) { IntPtr h = new IntPtr(id); return IsWindow(h) && IsWindowVisible(h); }
|
|
114
|
+
// PrintWindow with PW_RENDERFULLCONTENT: the window's own picture, also when other windows cover it.
|
|
115
|
+
// Cropped to the visible frame (without the invisible resize borders).
|
|
116
|
+
public static Bitmap Window(long id) {
|
|
117
|
+
IntPtr h = new IntPtr(id);
|
|
118
|
+
RECT r; if (!GetWindowRect(h, out r)) return null;
|
|
119
|
+
int w = r.R - r.L, ht = r.B - r.T;
|
|
120
|
+
if (w <= 1 || ht <= 1) return null;
|
|
121
|
+
Bitmap full = new Bitmap(w, ht, PixelFormat.Format32bppArgb);
|
|
122
|
+
using (Graphics g = Graphics.FromImage(full)) {
|
|
123
|
+
IntPtr hdc = g.GetHdc();
|
|
124
|
+
bool ok;
|
|
125
|
+
try { ok = PrintWindow(h, hdc, 2); } finally { g.ReleaseHdc(hdc); }
|
|
126
|
+
if (!ok) { full.Dispose(); return null; }
|
|
127
|
+
}
|
|
128
|
+
RECT f;
|
|
129
|
+
if (DwmGetWindowAttribute(h, 9, out f, Marshal.SizeOf(typeof(RECT))) == 0) {
|
|
130
|
+
Rectangle c = Rectangle.Intersect(new Rectangle(f.L - r.L, f.T - r.T, f.R - f.L, f.B - f.T), new Rectangle(0, 0, w, ht));
|
|
131
|
+
if (c.Width > 1 && c.Height > 1 && (c.Width < w || c.Height < ht)) { Bitmap crop = full.Clone(c, full.PixelFormat); full.Dispose(); return crop; }
|
|
132
|
+
}
|
|
133
|
+
return full;
|
|
134
|
+
}
|
|
135
|
+
public static Bitmap Scale(Bitmap src, double k) {
|
|
136
|
+
int w = Math.Max(1, (int)Math.Round(src.Width * k)), h = Math.Max(1, (int)Math.Round(src.Height * k));
|
|
137
|
+
Bitmap dst = new Bitmap(w, h, PixelFormat.Format24bppRgb);
|
|
138
|
+
using (Graphics g = Graphics.FromImage(dst)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.DrawImage(src, 0, 0, w, h); }
|
|
139
|
+
return dst;
|
|
140
|
+
}
|
|
141
|
+
static ImageCodecInfo Jpeg() { foreach (ImageCodecInfo c in ImageCodecInfo.GetImageEncoders()) if (c.MimeType == "image/jpeg") return c; return null; }
|
|
142
|
+
public static byte[] ToJpeg(Bitmap b, long quality) {
|
|
143
|
+
using (MemoryStream ms = new MemoryStream()) {
|
|
144
|
+
EncoderParameters p = new EncoderParameters(1); p.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
|
|
145
|
+
b.Save(ms, Jpeg(), p); return ms.ToArray();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Long side <= maxSide, then lower quality / size until it fits maxBytes.
|
|
149
|
+
public class Packed { public byte[] Data; public int W, H; public long Quality; }
|
|
150
|
+
public static Packed Pack(Bitmap src, int maxSide, int maxBytes) {
|
|
151
|
+
double k = Math.Min(1.0, (double)maxSide / Math.Max(src.Width, src.Height));
|
|
152
|
+
long[] qs = new long[] { 80, 65, 50 };
|
|
153
|
+
for (int round = 0; round < 6; round++) {
|
|
154
|
+
using (Bitmap s = Scale(src, k)) {
|
|
155
|
+
foreach (long q in qs) {
|
|
156
|
+
byte[] d = ToJpeg(s, q);
|
|
157
|
+
if (d.Length <= maxBytes) { Packed p = new Packed(); p.Data = d; p.W = s.Width; p.H = s.Height; p.Quality = q; return p; }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
k *= 0.75;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
// A NEW file only (CreateNew): never overwrites, also when the name was taken a moment ago.
|
|
165
|
+
public static void Save(Bitmap b, string path) {
|
|
166
|
+
string ext = Path.GetExtension(path).ToLowerInvariant();
|
|
167
|
+
byte[] data = ext == ".png" ? ToPng(b) : ToJpeg(b, 90);
|
|
168
|
+
FileStream f = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
|
169
|
+
try { f.Write(data, 0, data.Length); f.Dispose(); }
|
|
170
|
+
catch (Exception) { f.Dispose(); try { File.Delete(path); } catch (Exception) { } throw; }
|
|
171
|
+
}
|
|
172
|
+
public static byte[] ToPng(Bitmap b) { using (MemoryStream ms = new MemoryStream()) { b.Save(ms, ImageFormat.Png); return ms.ToArray(); } }
|
|
173
|
+
}
|
|
174
|
+
'@
|
|
175
|
+
|
|
176
|
+
# --- OCR (Windows.Media.Ocr, WinRT from PowerShell 5.1) ---------------------------------------
|
|
177
|
+
function Get-Await {
|
|
178
|
+
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
|
179
|
+
$script:AsTaskOp = [System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } | Select-Object -First 1
|
|
180
|
+
}
|
|
181
|
+
function Await($op, [Type]$type) {
|
|
182
|
+
$task = $script:AsTaskOp.MakeGenericMethod($type).Invoke($null, @($op))
|
|
183
|
+
[void]$task.Wait(-1)
|
|
184
|
+
$task.Result
|
|
185
|
+
}
|
|
186
|
+
function Get-OcrLanguages {
|
|
187
|
+
[void][Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
|
188
|
+
@([Windows.Media.Ocr.OcrEngine]::AvailableRecognizerLanguages | ForEach-Object { $_.LanguageTag })
|
|
189
|
+
}
|
|
190
|
+
function Invoke-Ocr([byte[]]$bytes, [string]$lang) {
|
|
191
|
+
Get-Await
|
|
192
|
+
[void][Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
|
193
|
+
[void][Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime]
|
|
194
|
+
[void][Windows.Graphics.Imaging.SoftwareBitmap, Windows.Foundation, ContentType = WindowsRuntime]
|
|
195
|
+
[void][Windows.Storage.Streams.IRandomAccessStream, Windows.Foundation, ContentType = WindowsRuntime]
|
|
196
|
+
$langs = @(Get-OcrLanguages) # @(): one language must stay a list, not a string
|
|
197
|
+
if (-not $langs.Count) { Fail 'NO_OCR_LANG' 'No text recognition language is installed in Windows.' }
|
|
198
|
+
if ($lang) {
|
|
199
|
+
$hit = $langs | Where-Object { $_ -ieq $lang -or $_ -ilike "$lang-*" } | Select-Object -First 1
|
|
200
|
+
if (-not $hit) { Fail 'BAD_LANG' "Text recognition for ""$lang"" is not installed." @{ languages = $langs } }
|
|
201
|
+
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage([Windows.Globalization.Language]::new($hit))
|
|
202
|
+
} else {
|
|
203
|
+
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
|
204
|
+
if (-not $engine) { $engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage([Windows.Globalization.Language]::new($langs[0])) }
|
|
205
|
+
}
|
|
206
|
+
if (-not $engine) { Fail 'NO_OCR_LANG' 'Text recognition could not start for the installed languages.' @{ languages = $langs } }
|
|
207
|
+
$ms = New-Object IO.MemoryStream (, $bytes)
|
|
208
|
+
$stream = [System.IO.WindowsRuntimeStreamExtensions]::AsRandomAccessStream($ms)
|
|
209
|
+
try {
|
|
210
|
+
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
|
211
|
+
} catch { Fail 'BAD_IMAGE' 'This file is not an image Windows can read (PNG, JPEG, BMP, GIF or TIFF).' }
|
|
212
|
+
$max = [Windows.Media.Ocr.OcrEngine]::MaxImageDimension
|
|
213
|
+
if ($decoder.PixelWidth -gt $max -or $decoder.PixelHeight -gt $max) { Fail 'IMAGE_TOO_LARGE' "The image is larger than $max pixels on a side." }
|
|
214
|
+
$bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
|
215
|
+
$res = Await ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
|
|
216
|
+
@{ language = $engine.RecognizerLanguage.LanguageTag; lines = @($res.Lines | ForEach-Object { $_.Text }); width = [int]$decoder.PixelWidth; height = [int]$decoder.PixelHeight }
|
|
217
|
+
}
|
|
218
|
+
# Text recognition works best on the full-size picture; above the engine's limit it is scaled down.
|
|
219
|
+
function Get-OcrBytes($bmp) {
|
|
220
|
+
[void][Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
|
221
|
+
$max = [Windows.Media.Ocr.OcrEngine]::MaxImageDimension
|
|
222
|
+
$side = [Math]::Max($bmp.Width, $bmp.Height)
|
|
223
|
+
if ($side -gt $max) { $s = [BrShot]::Scale($bmp, ($max - 1) / $side); try { return , [BrShot]::ToPng($s) } finally { $s.Dispose() } }
|
|
224
|
+
, [BrShot]::ToPng($bmp)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
[BrShot]::Dpi()
|
|
229
|
+
$action = $env:BRX_ACTION
|
|
230
|
+
if ($action -eq 'langs') { Out-Json @{ ok = $true; languages = @(Get-OcrLanguages) }; exit 0 }
|
|
231
|
+
if ($action -eq 'ocr' -and $env:BRX_FILE) {
|
|
232
|
+
$r = Invoke-Ocr ([IO.File]::ReadAllBytes($env:BRX_FILE)) $env:BRX_LANG
|
|
233
|
+
Out-Json (@{ ok = $true; kind = 'file' } + $r); exit 0
|
|
234
|
+
}
|
|
235
|
+
if (([BrShot]::InputDesktop()) -ne 'Default') { Fail 'SCREEN_LOCKED' 'The screen is locked, signed out or showing a Windows security prompt.' }
|
|
236
|
+
if ($action -eq 'windows') {
|
|
237
|
+
$wins = @([BrShot]::Windows() | ForEach-Object {
|
|
238
|
+
$p = ''; try { $p = (Get-Process -Id $_.Pid -ErrorAction Stop).ProcessName } catch { }
|
|
239
|
+
@{ id = $_.Id; pid = [int64]$_.Pid; title = $_.Title; process = $p; minimized = $_.Minimized; width = $_.W; height = $_.H } })
|
|
240
|
+
Out-Json @{ ok = $true; windows = $wins }; exit 0
|
|
241
|
+
}
|
|
242
|
+
if ($action -ne 'capture' -and $action -ne 'ocr') { Fail 'BAD_ACTION' "Unknown action $action." }
|
|
243
|
+
$hwnd = [long]('0' + $env:BRX_HWND)
|
|
244
|
+
$monitor = [int]('0' + $env:BRX_MONITOR)
|
|
245
|
+
$minimized = $false
|
|
246
|
+
if ($hwnd) {
|
|
247
|
+
if (-not [BrShot]::Exists($hwnd)) { Fail 'NO_WINDOW' 'That window is not open any more.' }
|
|
248
|
+
# The id was looked up in an earlier run: it must still belong to the same program (ids are reused).
|
|
249
|
+
if ($env:BRX_PID -and [string][BrShot]::Owner($hwnd) -ne $env:BRX_PID) { Fail 'NO_WINDOW' 'That window closed meanwhile.' }
|
|
250
|
+
$minimized = [BrShot]::IsMinimized($hwnd)
|
|
251
|
+
$bmp = [BrShot]::Window($hwnd)
|
|
252
|
+
$kind = 'window'
|
|
253
|
+
} else {
|
|
254
|
+
$count = ([BrShot]::Monitors()).Count
|
|
255
|
+
if ($monitor -gt $count) { Fail 'NO_MONITOR' "There is no monitor $monitor (this PC has $count)." @{ monitors = $count } }
|
|
256
|
+
try { $bmp = [BrShot]::Screen($monitor) } catch { Fail 'SCREEN_LOCKED' 'Windows gave no picture of the screen (locked, or a remote session without a display).' }
|
|
257
|
+
$kind = $(if ($monitor) { 'monitor' } else { 'screen' })
|
|
258
|
+
}
|
|
259
|
+
if (-not $bmp) { Fail 'CAPTURE_FAILED' 'Windows could not take the picture.' }
|
|
260
|
+
$script:taken = $true
|
|
261
|
+
try {
|
|
262
|
+
$out = @{ ok = $true; kind = $kind; width = $bmp.Width; height = $bmp.Height; minimized = $minimized }
|
|
263
|
+
if ($action -eq 'ocr') {
|
|
264
|
+
$r = Invoke-Ocr (Get-OcrBytes $bmp) $env:BRX_LANG
|
|
265
|
+
$out.language = $r.language; $out.lines = $r.lines
|
|
266
|
+
}
|
|
267
|
+
if ($env:BRX_IMAGE -eq '1') {
|
|
268
|
+
$p = [BrShot]::Pack($bmp, [int]$env:BRX_MAXSIDE, [int]$env:BRX_MAXBYTES)
|
|
269
|
+
if (-not $p) { Fail 'IMAGE_TOO_LARGE' 'The picture could not be made small enough.' }
|
|
270
|
+
$out.mime = 'image/jpeg'; $out.data = [Convert]::ToBase64String($p.Data); $out.outWidth = $p.W; $out.outHeight = $p.H; $out.bytes = $p.Data.Length
|
|
271
|
+
}
|
|
272
|
+
# The full-size copy last: no file is left on disk when an earlier step failed.
|
|
273
|
+
if ($env:BRX_SAVE) {
|
|
274
|
+
try { [BrShot]::Save($bmp, $env:BRX_SAVE) } catch {
|
|
275
|
+
$h = $_.Exception.HResult; if ($_.Exception.InnerException) { $h = $_.Exception.InnerException.HResult }
|
|
276
|
+
if ($h -eq -2147024816 -or $h -eq -2147024713) { Fail 'EXISTS' 'A file with this name appeared meanwhile; nothing was overwritten.' }
|
|
277
|
+
throw
|
|
278
|
+
}
|
|
279
|
+
$out.saved = $env:BRX_SAVE
|
|
280
|
+
}
|
|
281
|
+
Out-Json $out
|
|
282
|
+
} finally { $bmp.Dispose() }
|
|
283
|
+
} catch {
|
|
284
|
+
Out-Json @{ ok = $false; code = 'PS_ERROR'; message = [string]$_.Exception.Message; taken = [bool]$script:taken }
|
|
285
|
+
}
|