@remcp/runtime 0.2.154 → 0.2.155
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 +1 -1
- package/src/extended/browser-actions.mjs +311 -0
- package/src/extended/browser-endpoint.mjs +469 -0
- package/src/extended/browser-read.mjs +288 -0
- package/src/extended/browser-session.mjs +413 -0
- package/src/extended/browser.mjs +4 -1440
- package/src/extended/desktop-computer-actions.mjs +346 -0
- package/src/extended/desktop-computer-snapshot.mjs +245 -0
- package/src/extended/desktop-core.mjs +449 -0
- package/src/extended/desktop-linux-displays.mjs +366 -0
- package/src/extended/desktop-linux-input.mjs +485 -0
- package/src/extended/desktop-linux-screen.mjs +105 -0
- package/src/extended/desktop-linux-ui.mjs +407 -0
- package/src/extended/desktop-linux-window-actions.mjs +237 -0
- package/src/extended/desktop-linux-windows.mjs +321 -0
- package/src/extended/desktop-linux.mjs +6 -1791
- package/src/extended/desktop-ui-actions.mjs +402 -0
- package/src/extended/desktop-vision.mjs +209 -0
- package/src/extended/desktop.mjs +6 -1583
- package/src/tools/files-archive-actions.mjs +166 -0
- package/src/tools/files-archive-safety.mjs +395 -0
- package/src/tools/files-bulk-operations.mjs +415 -0
- package/src/tools/files-read-traversal.mjs +347 -0
- package/src/tools/files-screenshots.mjs +167 -0
- package/src/tools/files-secure-paths.mjs +408 -0
- package/src/tools/files-write-edit.mjs +336 -0
- package/src/tools/files.mjs +8 -2111
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { open } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
import { image, multi, text } from '../util.mjs';
|
|
6
|
+
import { capturePortalScreenshot, isWaylandSession } from '../screenshot-portal.mjs';
|
|
7
|
+
import { hasWaylandRemoteDesktopGrant, portalPointerButton, portalPointerMotion, portalPointerMotionAbsolute, portalScroll, portalShortcut, portalTypeText, waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
|
|
8
|
+
import {
|
|
9
|
+
clamp,
|
|
10
|
+
commandExists,
|
|
11
|
+
jsonResult,
|
|
12
|
+
optionalString,
|
|
13
|
+
readPrivateTempFile,
|
|
14
|
+
removeTemp,
|
|
15
|
+
requireEnum,
|
|
16
|
+
runFile,
|
|
17
|
+
spawnDetached,
|
|
18
|
+
spawnDetachedWithInput,
|
|
19
|
+
tempDir,
|
|
20
|
+
unavailable,
|
|
21
|
+
} from './common.mjs';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export function unwrapDbus(value) {
|
|
26
|
+
if (value && typeof value === 'object' && Object.hasOwn(value, 'value')) return unwrapDbus(value.value);
|
|
27
|
+
if (Array.isArray(value)) return value.map(unwrapDbus);
|
|
28
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, unwrapDbus(item)]));
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function mutterDisplayInventory() {
|
|
33
|
+
if (!isWaylandSession()) return null;
|
|
34
|
+
let dbus;
|
|
35
|
+
try { dbus = await import('@jellybrick/dbus-next'); } catch { return null; }
|
|
36
|
+
const bus = dbus.sessionBus();
|
|
37
|
+
try {
|
|
38
|
+
const object = await bus.getProxyObject('org.gnome.Mutter.DisplayConfig', '/org/gnome/Mutter/DisplayConfig');
|
|
39
|
+
const iface = object.getInterface('org.gnome.Mutter.DisplayConfig');
|
|
40
|
+
const state = unwrapDbus(await iface.GetCurrentState());
|
|
41
|
+
if (!Array.isArray(state) || state.length < 3) return null;
|
|
42
|
+
const monitors = Array.isArray(state[1]) ? state[1] : [];
|
|
43
|
+
const logical = Array.isArray(state[2]) ? state[2] : [];
|
|
44
|
+
const monitorByKey = new Map();
|
|
45
|
+
for (const monitor of monitors) {
|
|
46
|
+
const spec = Array.isArray(monitor?.[0]) ? monitor[0] : [];
|
|
47
|
+
const modes = Array.isArray(monitor?.[1]) ? monitor[1] : [];
|
|
48
|
+
const props = monitor?.[2] && typeof monitor[2] === 'object' ? monitor[2] : {};
|
|
49
|
+
const current = modes.find(mode => mode?.[6]?.['is-current'] === true)
|
|
50
|
+
|| modes.find(mode => mode?.[6]?.['is-preferred'] === true)
|
|
51
|
+
|| modes[0];
|
|
52
|
+
monitorByKey.set(JSON.stringify(spec), {
|
|
53
|
+
connector: String(spec[0] || ''),
|
|
54
|
+
vendor: String(spec[1] || ''),
|
|
55
|
+
product: String(spec[2] || ''),
|
|
56
|
+
serial: String(spec[3] || ''),
|
|
57
|
+
display_name: String(props['display-name'] || spec[0] || 'Display'),
|
|
58
|
+
builtin: Boolean(props['is-builtin']),
|
|
59
|
+
mode: current ? {
|
|
60
|
+
width: Number(current[1]) || null,
|
|
61
|
+
height: Number(current[2]) || null,
|
|
62
|
+
refresh_hz: Number(current[3]) || null,
|
|
63
|
+
} : null,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const rows = [];
|
|
67
|
+
for (const item of logical) {
|
|
68
|
+
if (!Array.isArray(item) || item.length < 6) continue;
|
|
69
|
+
const x = Number(item[0]), y = Number(item[1]), scale = Number(item[2]) || 1;
|
|
70
|
+
const transform = Number(item[3]) || 0;
|
|
71
|
+
const primary = Boolean(item[4]);
|
|
72
|
+
const specs = Array.isArray(item[5]) ? item[5] : [];
|
|
73
|
+
for (const spec of specs) {
|
|
74
|
+
const details = monitorByKey.get(JSON.stringify(spec)) || {
|
|
75
|
+
connector: String(spec?.[0] || ''),
|
|
76
|
+
vendor: String(spec?.[1] || ''),
|
|
77
|
+
product: String(spec?.[2] || ''),
|
|
78
|
+
serial: String(spec?.[3] || ''),
|
|
79
|
+
display_name: String(spec?.[0] || 'Display'),
|
|
80
|
+
mode: null,
|
|
81
|
+
};
|
|
82
|
+
const pixelWidth = Number(details.mode?.width) || null;
|
|
83
|
+
const pixelHeight = Number(details.mode?.height) || null;
|
|
84
|
+
const rotated = [1,3,5,7].includes(transform);
|
|
85
|
+
rows.push({
|
|
86
|
+
name: details.connector || details.display_name,
|
|
87
|
+
display_name: details.display_name,
|
|
88
|
+
primary,
|
|
89
|
+
x: Number.isFinite(x) ? x : null,
|
|
90
|
+
y: Number.isFinite(y) ? y : null,
|
|
91
|
+
width: pixelWidth == null ? null : Math.round((rotated ? pixelHeight : pixelWidth) / scale),
|
|
92
|
+
height: pixelHeight == null ? null : Math.round((rotated ? pixelWidth : pixelHeight) / scale),
|
|
93
|
+
pixel_width: pixelWidth,
|
|
94
|
+
pixel_height: pixelHeight,
|
|
95
|
+
scale,
|
|
96
|
+
refresh_hz: details.mode?.refresh_hz ?? null,
|
|
97
|
+
transform,
|
|
98
|
+
builtin: Boolean(details.builtin),
|
|
99
|
+
vendor: details.vendor || null,
|
|
100
|
+
product: details.product || null,
|
|
101
|
+
serial: details.serial || null,
|
|
102
|
+
backend: 'mutter',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return rows.length ? rows : null;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
} finally {
|
|
110
|
+
try { bus.disconnect(); } catch {}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function parseWlrRandrOutput(stdout) {
|
|
115
|
+
const rows = [];
|
|
116
|
+
let current = null;
|
|
117
|
+
|
|
118
|
+
const finish = () => {
|
|
119
|
+
if (!current || current.enabled === false) return;
|
|
120
|
+
const mode = current.currentMode || current.firstMode;
|
|
121
|
+
if (!mode) return;
|
|
122
|
+
const scale = Number.isFinite(current.scale) && current.scale > 0 ? current.scale : 1;
|
|
123
|
+
const rotated = /(?:^|\D)(?:90|270)(?:\D|$)/.test(current.transform || '');
|
|
124
|
+
const pixelWidth = mode.width;
|
|
125
|
+
const pixelHeight = mode.height;
|
|
126
|
+
rows.push({
|
|
127
|
+
name:current.name,
|
|
128
|
+
display_name:current.description || current.model || current.name,
|
|
129
|
+
primary:false,
|
|
130
|
+
x:Number.isFinite(current.x) ? current.x : null,
|
|
131
|
+
y:Number.isFinite(current.y) ? current.y : null,
|
|
132
|
+
width:Math.round((rotated ? pixelHeight : pixelWidth) / scale),
|
|
133
|
+
height:Math.round((rotated ? pixelWidth : pixelHeight) / scale),
|
|
134
|
+
pixel_width:pixelWidth,
|
|
135
|
+
pixel_height:pixelHeight,
|
|
136
|
+
scale,
|
|
137
|
+
refresh_hz:mode.refresh_hz,
|
|
138
|
+
transform:current.transform || 'normal',
|
|
139
|
+
builtin:/^(?:eDP|LVDS|DSI)-/i.test(current.name),
|
|
140
|
+
vendor:current.make || null,
|
|
141
|
+
product:current.model || null,
|
|
142
|
+
serial:current.serial || null,
|
|
143
|
+
backend:'wlr-randr',
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const rawLine of String(stdout || '').split(/\r?\n/)) {
|
|
148
|
+
if (rawLine && !/^\s/.test(rawLine)) {
|
|
149
|
+
finish();
|
|
150
|
+
const header = rawLine.match(/^(\S+)(?:\s+"([^"]*)")?\s*$/);
|
|
151
|
+
current = header ? {
|
|
152
|
+
name:header[1],
|
|
153
|
+
description:header[2] || '',
|
|
154
|
+
enabled:true,
|
|
155
|
+
scale:1,
|
|
156
|
+
transform:'normal',
|
|
157
|
+
currentMode:null,
|
|
158
|
+
firstMode:null,
|
|
159
|
+
} : null;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!current) continue;
|
|
163
|
+
const line = rawLine.trim();
|
|
164
|
+
let match;
|
|
165
|
+
if ((match = line.match(/^Enabled:\s*(yes|no)$/i))) current.enabled = match[1].toLowerCase() === 'yes';
|
|
166
|
+
else if ((match = line.match(/^Position:\s*(-?\d+)\s*,\s*(-?\d+)$/i))) {
|
|
167
|
+
current.x = Number(match[1]);
|
|
168
|
+
current.y = Number(match[2]);
|
|
169
|
+
} else if ((match = line.match(/^Scale:\s*(\d+(?:\.\d+)?)$/i))) current.scale = Number(match[1]);
|
|
170
|
+
else if ((match = line.match(/^Transform:\s*(.+)$/i))) current.transform = match[1].trim();
|
|
171
|
+
else if ((match = line.match(/^Make:\s*(.*)$/i))) current.make = match[1].trim();
|
|
172
|
+
else if ((match = line.match(/^Model:\s*(.*)$/i))) current.model = match[1].trim();
|
|
173
|
+
else if ((match = line.match(/^Serial:\s*(.*)$/i))) current.serial = match[1].trim();
|
|
174
|
+
else if ((match = line.match(/^(\d+)x(\d+)\s+px,\s*([\d.]+)\s+Hz(?:\s+\(([^)]*)\))?/i))) {
|
|
175
|
+
const mode = { width:Number(match[1]), height:Number(match[2]), refresh_hz:Number(match[3]) };
|
|
176
|
+
current.firstMode ||= mode;
|
|
177
|
+
if (/\bcurrent\b/i.test(match[4] || '')) current.currentMode = mode;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finish();
|
|
181
|
+
return rows;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function parseXrandrDisplayOutput(stdout, scale = 1) {
|
|
185
|
+
const rows = [];
|
|
186
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
187
|
+
const match = line.match(/^(\S+) connected( primary)?(?: (\d+)x(\d+)\+(-?\d+)\+(-?\d+))?/);
|
|
188
|
+
if (match) rows.push({
|
|
189
|
+
name:match[1],
|
|
190
|
+
display_name:match[1],
|
|
191
|
+
primary:Boolean(match[2]),
|
|
192
|
+
width:match[3]?Number(match[3]):null,
|
|
193
|
+
height:match[4]?Number(match[4]):null,
|
|
194
|
+
pixel_width:match[3]?Number(match[3]):null,
|
|
195
|
+
pixel_height:match[4]?Number(match[4]):null,
|
|
196
|
+
x:match[5]?Number(match[5]):null,
|
|
197
|
+
y:match[6]?Number(match[6]):null,
|
|
198
|
+
scale,
|
|
199
|
+
backend:'xrandr',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return rows;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function xrandrDisplayScale({ commandExistsFn = commandExists, runFileFn = runFile, env = process.env } = {}) {
|
|
206
|
+
let scale = Number(env.GDK_SCALE || env.QT_SCALE_FACTOR || 0);
|
|
207
|
+
if (!Number.isFinite(scale) || scale <= 0) scale = 0;
|
|
208
|
+
if (!scale && commandExistsFn('gsettings')) {
|
|
209
|
+
const probe = await runFileFn('gsettings', ['get','org.gnome.desktop.interface','scaling-factor'], { label:'display scale', allowFailure:true, timeout:1500 });
|
|
210
|
+
const parsed = Number(String(probe.stdout || '').match(/(\d+(?:\.\d+)?)/)?.[1]);
|
|
211
|
+
if (Number.isFinite(parsed) && parsed > 0) scale = parsed;
|
|
212
|
+
}
|
|
213
|
+
return scale || 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function resolveLinuxDisplayInventory({
|
|
217
|
+
wayland = isWaylandSession(),
|
|
218
|
+
mutterProbe = mutterDisplayInventory,
|
|
219
|
+
commandExistsFn = commandExists,
|
|
220
|
+
runFileFn = runFile,
|
|
221
|
+
env = process.env,
|
|
222
|
+
} = {}) {
|
|
223
|
+
if (wayland) {
|
|
224
|
+
const mutter = await mutterProbe().catch(() => null);
|
|
225
|
+
if (Array.isArray(mutter) && mutter.length) return mutter;
|
|
226
|
+
|
|
227
|
+
if (commandExistsFn('wlr-randr')) {
|
|
228
|
+
const result = await runFileFn('wlr-randr', [], { label:'display inventory', allowFailure:true, timeout:2500, maxBuffer:4*1024*1024 });
|
|
229
|
+
if (result.code === 0) {
|
|
230
|
+
const rows = parseWlrRandrOutput(result.stdout);
|
|
231
|
+
if (rows.length) return rows;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (commandExistsFn('xrandr')) {
|
|
237
|
+
const result = await runFileFn('xrandr', ['--query'], { label:'display inventory', allowFailure:true, timeout:2500, maxBuffer:4*1024*1024 });
|
|
238
|
+
if (result.code === 0) {
|
|
239
|
+
const scale = await xrandrDisplayScale({ commandExistsFn, runFileFn, env });
|
|
240
|
+
const rows = parseXrandrDisplayOutput(result.stdout, scale);
|
|
241
|
+
if (rows.length) return rows;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let displayCapabilityCache = { value:false, expiresAt:0, pending:null };
|
|
248
|
+
|
|
249
|
+
export async function displayInventoryAvailable(options) {
|
|
250
|
+
if (options && Object.keys(options).length) {
|
|
251
|
+
return Boolean((await resolveLinuxDisplayInventory(options))?.length);
|
|
252
|
+
}
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
if (displayCapabilityCache.expiresAt > now) return displayCapabilityCache.value;
|
|
255
|
+
if (displayCapabilityCache.pending) return displayCapabilityCache.pending;
|
|
256
|
+
const pending = resolveLinuxDisplayInventory()
|
|
257
|
+
.then(rows => {
|
|
258
|
+
const value = Boolean(rows?.length);
|
|
259
|
+
displayCapabilityCache = { value, expiresAt:Date.now() + 5000, pending:null };
|
|
260
|
+
return value;
|
|
261
|
+
})
|
|
262
|
+
.catch(() => {
|
|
263
|
+
displayCapabilityCache = { value:false, expiresAt:Date.now() + 5000, pending:null };
|
|
264
|
+
return false;
|
|
265
|
+
});
|
|
266
|
+
displayCapabilityCache.pending = pending;
|
|
267
|
+
return pending;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function displayInventory() {
|
|
271
|
+
const rows = await resolveLinuxDisplayInventory();
|
|
272
|
+
if (!rows?.length) unavailable('Display inventory', 'GNOME Mutter DisplayConfig, wlr-randr or xrandr is required');
|
|
273
|
+
return jsonResult(rows);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function pngDimensions(file) {
|
|
277
|
+
const handle = await open(file, 'r');
|
|
278
|
+
try {
|
|
279
|
+
const buffer = Buffer.alloc(24);
|
|
280
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
281
|
+
if (bytesRead < buffer.length || buffer.subarray(0, 8).toString('hex') !== '89504e470d0a1a0a') return null;
|
|
282
|
+
const width = buffer.readUInt32BE(16);
|
|
283
|
+
const height = buffer.readUInt32BE(20);
|
|
284
|
+
if (!width || !height) return null;
|
|
285
|
+
return { width, height };
|
|
286
|
+
} finally {
|
|
287
|
+
await handle.close();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function regionScreenshotBackends({
|
|
292
|
+
wayland = isWaylandSession(),
|
|
293
|
+
grim = commandExists('grim'),
|
|
294
|
+
gnomeScreenshot = commandExists('gnome-screenshot'),
|
|
295
|
+
ffmpeg = commandExists('ffmpeg'),
|
|
296
|
+
imagemagick = commandExists('import'),
|
|
297
|
+
portalCandidate = waylandPortalCandidate(),
|
|
298
|
+
} = {}) {
|
|
299
|
+
const backends = [];
|
|
300
|
+
if (wayland && grim) backends.push('grim');
|
|
301
|
+
if (wayland) {
|
|
302
|
+
if (gnomeScreenshot && ffmpeg) backends.push('gnome-screenshot');
|
|
303
|
+
if (ffmpeg && portalCandidate) backends.push('portal');
|
|
304
|
+
} else if (imagemagick) {
|
|
305
|
+
backends.push('imagemagick');
|
|
306
|
+
}
|
|
307
|
+
return backends;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function portalCropGeometry(rect, displays = [], bitmap = null) {
|
|
311
|
+
const requested = {
|
|
312
|
+
x: Math.trunc(Number(rect?.x)),
|
|
313
|
+
y: Math.trunc(Number(rect?.y)),
|
|
314
|
+
width: Math.trunc(Number(rect?.width)),
|
|
315
|
+
height: Math.trunc(Number(rect?.height)),
|
|
316
|
+
};
|
|
317
|
+
if (![requested.x,requested.y,requested.width,requested.height].every(Number.isFinite)
|
|
318
|
+
|| requested.width <= 0 || requested.height <= 0) {
|
|
319
|
+
throw new Error('portal crop requires a finite positive rectangle');
|
|
320
|
+
}
|
|
321
|
+
const visibleDisplays = Array.isArray(displays) ? displays.filter(item => {
|
|
322
|
+
const x = Number(item?.x), y = Number(item?.y), width = Number(item?.width), height = Number(item?.height);
|
|
323
|
+
return [x,y,width,height].every(Number.isFinite) && width > 0 && height > 0;
|
|
324
|
+
}) : [];
|
|
325
|
+
const bitmapWidth = Math.trunc(Number(bitmap?.width));
|
|
326
|
+
const bitmapHeight = Math.trunc(Number(bitmap?.height));
|
|
327
|
+
const hasBitmap = Number.isFinite(bitmapWidth) && bitmapWidth > 0 && Number.isFinite(bitmapHeight) && bitmapHeight > 0;
|
|
328
|
+
|
|
329
|
+
let minX = 0, minY = 0;
|
|
330
|
+
let maxX = hasBitmap ? bitmapWidth : requested.x + requested.width;
|
|
331
|
+
let maxY = hasBitmap ? bitmapHeight : requested.y + requested.height;
|
|
332
|
+
if (visibleDisplays.length) {
|
|
333
|
+
minX = Math.min(...visibleDisplays.map(item => Number(item.x)));
|
|
334
|
+
minY = Math.min(...visibleDisplays.map(item => Number(item.y)));
|
|
335
|
+
maxX = Math.max(...visibleDisplays.map(item => Number(item.x) + Number(item.width)));
|
|
336
|
+
maxY = Math.max(...visibleDisplays.map(item => Number(item.y) + Number(item.height)));
|
|
337
|
+
}
|
|
338
|
+
const left = Math.max(requested.x, minX);
|
|
339
|
+
const top = Math.max(requested.y, minY);
|
|
340
|
+
const right = Math.min(requested.x + requested.width, maxX);
|
|
341
|
+
const bottom = Math.min(requested.y + requested.height, maxY);
|
|
342
|
+
if (right <= left || bottom <= top) throw new Error('screenshot region is outside visible desktop bounds');
|
|
343
|
+
|
|
344
|
+
const virtualWidth = maxX - minX;
|
|
345
|
+
const virtualHeight = maxY - minY;
|
|
346
|
+
const scaleX = hasBitmap && virtualWidth > 0 ? bitmapWidth / virtualWidth : 1;
|
|
347
|
+
const scaleY = hasBitmap && virtualHeight > 0 ? bitmapHeight / virtualHeight : 1;
|
|
348
|
+
const x = Math.max(0, Math.floor((left - minX) * scaleX));
|
|
349
|
+
const y = Math.max(0, Math.floor((top - minY) * scaleY));
|
|
350
|
+
const sourceRight = hasBitmap
|
|
351
|
+
? Math.min(bitmapWidth, Math.ceil((right - minX) * scaleX))
|
|
352
|
+
: Math.ceil((right - minX) * scaleX);
|
|
353
|
+
const sourceBottom = hasBitmap
|
|
354
|
+
? Math.min(bitmapHeight, Math.ceil((bottom - minY) * scaleY))
|
|
355
|
+
: Math.ceil((bottom - minY) * scaleY);
|
|
356
|
+
const width = sourceRight - x;
|
|
357
|
+
const height = sourceBottom - y;
|
|
358
|
+
if (width <= 0 || height <= 0) throw new Error('screenshot region has no visible portal pixels');
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
x, y, width, height,
|
|
362
|
+
visible: { x:left, y:top, width:right-left, height:bottom-top },
|
|
363
|
+
clipped: left !== requested.x || top !== requested.y
|
|
364
|
+
|| right !== requested.x + requested.width || bottom !== requested.y + requested.height,
|
|
365
|
+
};
|
|
366
|
+
}
|