node-native-win-utils 2.2.0 → 2.2.3

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 CHANGED
@@ -1,684 +1,177 @@
1
-
2
- [![License][license-src]][license-href]
3
- ![Node-API v8 Badge](https://github.com/nodejs/abi-stable-node/blob/doc/assets/Node-API%20v8%20Badge.svg)
1
+ [![License][license-src]][license-href]
2
+ [![Node-API v8](https://raw.githubusercontent.com/nodejs/abi-stable-node/b062e657e639aad36280e0c6f54e181563ef4b19/assets/Node-API%20v8%20Badge.svg)](https://nodejs.org/api/n-api.html)
3
+ [![npm version](https://img.shields.io/npm/v/node-native-win-utils.svg)](https://www.npmjs.com/package/node-native-win-utils)
4
+ [![npm downloads](https://img.shields.io/npm/dm/node-native-win-utils.svg)](https://www.npmjs.com/package/node-native-win-utils)
5
+ [![Platform: Windows only](https://img.shields.io/badge/platform-Windows%20only-blue.svg)](https://github.com/T-Rumibul/node-native-win-utils)
4
6
 
5
7
  # Node Native Win Utils
6
8
 
7
-
8
-
9
9
  I did it for myself because I didn't feel like dealing with libraries like 'node-ffi' to implement this functionality. Maybe someone will find it useful. It's WINDOWS OS ONLY
10
10
 
11
-
12
-
13
- This package is a native addon for Node.js that allows you to perform various utility operations on Windows systems. It includes key event listeners, window data retrieval, window screenshot capture functionality, mouse movement, mouse click, mouse drag, and typing functionality, also I included precompiled libs of OpenCV(core, imgcodecs, imgproc)
14
-
15
-
16
- # Installation
17
-
18
-
19
11
 
20
- You can install the package using npm:
12
+ ## Features
21
13
 
22
-
14
+ - Global keyboard event listener (`keyDown` / `keyUp`)
15
+ - Window information & screenshots
16
+ - Mouse movement, clicks, drag & drop
17
+ - Keyboard emulation (`keyPress`, `typeString`)
18
+ - OpenCV integration (template matching, blur, grayscale, histogram equalization, color manipulation, ROI, drawing)
19
+ - Tesseract OCR text recognition
20
+ - Screen capture
21
+ - Mouse event listener
22
+ - Prebuilt binaries (no Python or build tools required on Windows)
23
+ - ESM + CommonJS support
23
24
 
24
- ```shell
25
+ ## Requirements
25
26
 
26
-
27
+ - **Windows 10 or later** (x64)
28
+ - Node.js >= 18 (prebuilts for recent versions via `prebuildify`)
27
29
 
28
- npm install node-native-win-utils
29
-
30
-
30
+ ## Installation
31
31
 
32
+ ```bash
33
+ npm install node-native-win-utils
32
34
  ```
33
35
 
34
-
35
-
36
- # Usage
37
-
38
-
39
-
40
- ## Importing the Package
41
-
42
-
43
-
44
- To use the package, import the necessary functions, types, and classes:
45
-
46
-
47
-
48
- ```javascript
36
+ ## Usage
49
37
 
38
+ ```ts
50
39
  import {
51
-
52
- KeyboardListener
53
-
54
- getWindowData,
55
-
56
- captureWindow,
57
-
58
- captureWindowN,
59
-
60
- mouseMove,
61
-
62
- mouseClick,
63
-
64
- mouseDrag,
65
-
66
- typeString,
67
-
68
- OpenCV,
69
-
70
- } from "node-native-win-utils";
71
-
40
+ KeyboardListener,
41
+ KeyCodeHelper,
42
+ KeyListener,
43
+ getWindowData,
44
+ captureWindow,
45
+ captureWindowN,
46
+ captureScreenToFile,
47
+ mouseMove,
48
+ mouseClick,
49
+ mouseDrag,
50
+ typeString,
51
+ keyPress,
52
+ textRecognition,
53
+ OpenCV,
54
+ startMouseListener,
55
+ stopMouseListener,
56
+ } from "node-native-win-utils";
72
57
  ```
73
58
 
74
-
75
-
76
- ## Keyboard Event Listener
77
-
78
-
79
-
80
- The package provides KeyboardListener singletone class, which allow you to register event listeners for key down and key up events, respectively. The event callbacks receive the `keyCode` as a parameter:
59
+ ### Keyboard
81
60
 
82
-
61
+ ```ts
62
+ // Singleton listener
63
+ const listener = KeyboardListener.listener();
64
+ listener.on("keyDown", (data) => console.log("Down:", data.keyName));
65
+ listener.on("keyUp", (data) => console.log("Up:", data.keyName));
83
66
 
84
- ```javascript
85
-
86
- const listener = KeyboardListener.listener()
87
-
88
- listener.on("keyDown", (data: {
89
- keyCode: number;
90
- keyName: string;
91
- }) => {
92
- //your code
93
- })
94
- listener.on("keyUp", (data: {
95
- keyCode: number;
96
- keyName: string;
97
- }) => {
98
- //your code
99
- })
67
+ // Simulate key press
68
+ keyPress(KeyCodeHelper.A); // once
69
+ keyPress(KeyCodeHelper.Enter, 2); // twice
100
70
  ```
101
71
 
102
- ## Key Press
103
-
104
-
105
-
106
- The keyPress function allows you to simulate a key press event. Provide the keyCode as a parameter and optionally the number of times to press the key:
107
-
108
-
109
-
110
- ```javascript
72
+ ### Mouse & Typing
111
73
 
112
- keyPress(65); // Press 'A' key once
113
-
114
-
115
-
116
- keyPress(65, 3); // Press 'A' key three times
74
+ ```ts
75
+ mouseMove(500, 300);
76
+ mouseClick(); // left click
77
+ mouseClick("right");
78
+ mouseClick("left", "down") // left button down
79
+ mouseClick("left", "up") // left button up
80
+ mouseDrag(100, 100, 800, 600, 50); // optional speed
117
81
 
82
+ typeString("Hello from Node!", 30); // 30ms delay per char
118
83
  ```
119
84
 
120
- ## Key Code Helper
121
-
122
-
123
-
124
- The KeyCodeHelper enum provides a set of key codes that can be used with key event functions:
125
-
126
-
127
-
128
- ```javascript
129
-
130
-
131
-
132
- import { KeyCodeHelper } from "node-native-win-utils";
133
-
134
-
135
-
136
- console.log(KeyCodeHelper.A); // Outputs the key code for 'A'
137
-
138
- ```
139
-
140
-
141
-
142
- ## Window Data
143
-
144
-
145
-
146
- The `getWindowData` function retrieves information about a specific window identified by its name. It returns an object with properties `width`, `height`, `x`, and `y`, representing the window dimensions and position:
147
-
148
-
149
-
150
- ```javascript
151
-
152
- const windowData = getWindowData("Window Name");
153
-
154
-
155
-
156
- console.log("Window data:", windowData);
157
-
158
-
159
-
160
- // Window data: { width: 800, height: 600, x: 50, y: 50 }
161
-
162
- ```
163
-
164
-
165
-
166
- ## Window Capture
167
-
168
-
169
-
170
- The `captureWindow` function allows you to capture a screenshot of a specific window identified by its name. Provide the window name and the output path as parameters:
171
-
172
-
173
-
174
- ```javascript
175
-
176
- captureWindow("Window Name", "output.png");
177
-
178
-
179
-
180
- // Output: output.png with a screenshot of the window
181
-
182
- ```
183
-
184
-
185
-
186
- ## Mouse Movement
187
-
188
-
189
-
190
- The `mouseMove` function allows you to move the mouse to a specific position on the screen. Provide the `posX` and `posY` coordinates as parameters:
191
-
192
-
193
-
194
- ```javascript
85
+ ### Window Operations
195
86
 
196
- mouseMove(100, 200);
87
+ ```ts
88
+ const data = getWindowData("Notepad");
89
+ console.log(data); // { width, height, x, y }
197
90
 
91
+ captureWindow("Notepad", "screenshot.png");
92
+ const buffer = captureWindowN("Notepad"); // Buffer
198
93
  ```
199
94
 
200
-
201
-
202
- ## Mouse Click
203
-
204
-
205
-
206
- The `mouseClick` function allows you to perform a mouse click event. Optionally, you can specify the mouse button as a parameter (`"left"`, `"middle"`, or `"right"`). If no button is specified, a left mouse click is performed by default:
207
-
208
-
209
-
210
- ```javascript
211
-
212
- mouseClick(); // Left mouse click
213
-
214
-
215
-
216
- mouseClick("right"); // Right mouse click
95
+ ### Screen Capture
217
96
 
97
+ ```ts
98
+ await captureScreenToFile("full-screen.png");
218
99
  ```
219
100
 
220
-
101
+ ### Text Recognition (Tesseract OCR)
221
102
 
222
- ## Mouse Drag
223
-
224
-
225
-
226
- The `mouseDrag` function allows you to simulate dragging the mouse from one position to another. Provide the starting and ending coordinates (`startX`, `startY`, `endX`, `endY`) as parameters. Optionally, you can specify the speed at which the mouse should be dragged:
227
-
228
-
229
-
230
- ```javascript
231
-
232
- mouseDrag(100, 200, 300, 400);
233
-
234
-
235
-
236
- mouseDrag(100, 200, 300, 400, 100); // Drag with speed 100
103
+ ```ts
104
+ import path from "path";
237
105
 
106
+ const text = textRecognition(
107
+ path.join(__dirname, "traineddata"), // path to .traineddata files
108
+ "eng",
109
+ path.join(__dirname, "image.png")
110
+ );
111
+ console.log(text);
238
112
  ```
239
113
 
240
-
114
+ ### OpenCV (image processing)
241
115
 
242
- ## Typing
116
+ ```ts
117
+ import { OpenCV } from "node-native-win-utils";
243
118
 
244
-
119
+ const img = new OpenCV("image.png"); // or ImageData { width, height, data: Uint8Array }
245
120
 
246
- The `typeString` function allows you to simulate typing a string of characters. Provide the string to type as the `stringToType` parameter. Optionally, you can specify
121
+ // Chainable methods
122
+ const processed = img
123
+ .blur(5, 5)
124
+ .bgrToGray()
125
+ .equalizeHist()
126
+ .darkenColor([240, 240, 240], [255, 255, 255], 0.5) // lower, upper, factor
127
+ .drawRectangle([10, 10], [200, 100], [255, 0, 0], 3)
128
+ .getRegion([50, 50, 300, 200]);
247
129
 
248
-
249
-
250
- a delay between each character (in milliseconds) using the `delay` parameter:
251
-
252
-
253
-
254
- ```javascript
255
-
256
- typeString("Hello, world!");
257
-
258
-
259
-
260
- typeString("Hello, world!", 100); // Type with a delay of 100ms between characters
130
+ processed.imwrite("processed.png");
261
131
 
132
+ // Template matching
133
+ const match = img.matchTemplate(templateImage, /* method */, /* mask */);
134
+ console.log(match); // { minValue, maxValue, minLocation, maxLocation }
262
135
  ```
263
136
 
264
-
265
-
266
- ## Key Listener Class
267
-
268
-
269
-
270
- The `KeyListener` class extends the EventEmitter class and simplifies working with the `keyDownHandler` and `keyUpHandler` functions. You can register event listeners for the "keyDown" and "keyUp" events using the `on` method:
271
-
272
-
273
-
274
- ```javascript
275
-
276
- const listener = new KeyListener();
277
-
278
-
279
-
280
- listener.on("keyDown", (data) => {
281
-
282
- console.log("Key down:", data.keyCode, data.keyName);
137
+ ### Mouse Event Listener
283
138
 
139
+ ```ts
140
+ startMouseListener(({ x, y, type }) => {
141
+ console.log(`${type} at ${x},${y}`); // move at 400 300
284
142
  });
285
143
 
286
-
287
-
288
- // Key down: 8 Backspace
289
-
290
-
291
-
292
- listener.on("keyUp", (data) => {
293
-
294
- console.log("Key up:", data.keyCode, data.keyName);
295
-
296
- });
297
-
298
-
299
-
300
- // Key up: 8 Backspace
301
-
302
- ```
303
-
304
-
305
-
306
- ## OpenCV
307
-
308
-
309
-
310
- The `OpenCV` class extends the capabilities of the native addon package by providing various image processing functionalities. It allows users to perform operations such as matching templates, blurring images, converting color formats, drawing rectangles, getting image regions, and writing images to files.
311
-
312
-
313
-
314
- #### Constructor
315
-
316
-
317
-
318
- ```typescript
319
-
320
- const image = new OpenCV(image: string | ImageData)
144
+ mouseClick("left"); // trigger event
321
145
 
146
+ // terminates a thread
147
+ stopMouseListener();
322
148
  ```
323
149
 
324
-
325
-
326
- Creates a new instance of the `OpenCV` class with the specified image data. The `image` parameter can be either a file path (string) or an existing `ImageData` object.
327
-
328
-
329
-
330
- #### Properties
331
-
332
-
333
-
334
- ##### `imageData: ImageData`
335
-
336
-
337
-
338
- Holds the underlying image data that will be used for image processing operations.
339
-
340
-
341
-
342
- ##### `width: number`
343
-
344
-
345
-
346
- Read-only property that returns the width of the image in pixels.
347
-
348
-
349
-
350
- ##### `height: number`
351
-
352
-
353
-
354
- Read-only property that returns the height of the image in pixels.
355
-
356
-
357
-
358
- #### Methods
359
-
360
-
361
-
362
- ##### `matchTemplate(template: ImageData, method?: number | null, mask?: ImageData): OpenCV`
363
-
364
-
365
-
366
- Matches a template image with the current image and returns a new `OpenCV` instance containing the result.
367
-
368
-
369
-
370
- - `template: ImageData`: The template image data to be matched.
371
-
372
- - `method?: number | null`: (Optional) The matching method to be used. If not provided, the default method will be used.(currently no implemented)
373
-
374
- - `mask?: ImageData`: (Optional) An optional mask image data to be used during the matching process.
375
-
376
-
377
-
378
- ##### `blur(sizeX: number, sizeY: number): OpenCV`
379
-
380
-
381
-
382
- Applies a blur filter to the current image and returns a new `OpenCV` instance containing the blurred result.
383
-
384
-
385
-
386
- - `sizeX: number`: The size of the blur kernel in the X direction.
387
-
388
- - `sizeY: number`: The size of the blur kernel in the Y direction.
389
-
390
-
391
-
392
- ##### `bgrToGray(): OpenCV`
393
-
394
-
395
-
396
- Converts the current image from the BGR color format to grayscale and returns a new `OpenCV` instance containing the grayscale result.
397
-
398
-
399
-
400
- ##### `drawRectangle(start: Point, end: Point, rgb: Color, thickness: number): OpenCV`
401
-
402
-
403
-
404
- Draws a rectangle on the current image and returns a new `OpenCV` instance containing the modified result.
405
-
406
-
407
-
408
- - `start: Point`: The starting point (top-left) of the rectangle.
409
-
410
- - `end: Point`: The ending point (bottom-right) of the rectangle.
411
-
412
- - `rgb: Color`: The color of the rectangle in the RGB format (e.g., `{ r: 255, g: 0, b: 0 }` for red).
413
-
414
- - `thickness: number`: The thickness of the rectangle's border.
415
-
416
-
417
-
418
- ##### `getRegion(region: ROI): OpenCV`
419
-
420
-
421
-
422
- Extracts a region of interest (ROI) from the current image and returns a new `OpenCV` instance containing the extracted region.
423
-
424
-
425
-
426
- - `region: ROI`: An object specifying the region of interest with properties `x`, `y`, `width`, `height`.
427
-
428
-
429
-
430
- ##### `imwrite(path: string): void`
431
-
432
-
433
-
434
- Writes the current image to a file specified by the `path`.
435
-
436
-
437
-
438
- - `path: string`: The file path where the image will be saved.
439
-
440
-
441
-
442
- ## Functions
443
-
444
-
445
-
446
- | Function | Parameters | Return Type |
447
- |-----------------|----------------------------------------------------------------------------------------------|-------------|
448
- | getWindowData | `windowName: string` | `WindowData`|
449
- | captureWindow | `windowName: string, outputPath: string` | `void` |
450
- | mouseMove | `posX: number, posY: number` | `boolean` |
451
- | mouseClick | `button?: "left" \| "middle" \| "right"` | `boolean` |
452
- | mouseDrag | `startX: number, startY: number, endX: number, endY: number, speed?: number` | `boolean` |
453
- | typeString | `stringToType: string, delay?: number` | `boolean` |
454
- | captureWindowN | `windowName: string` | `Buffer` |
455
- | keyPress | `keyCode: number, repeat?: number` | `boolean` |
456
- | textRecognition | `trainedDataPath: string, dataLang: string, imagePath: string` | `string` |
457
- | captureScreenToFile | `path: string` | `boolean` |
458
-
459
-
460
- ## Examples
461
-
462
-
463
-
464
- Here are some examples of using the package:
465
-
466
- ```javascript
467
- console.log(textRecognition(path.join(__dirname, 'traineddata'), 'eng', path.join(__dirname, 'images', '1.jpg'))) // ---> <recognized text>
150
+ ## Building from source
468
151
 
152
+ ```bash
153
+ npm install
154
+ npm run build
469
155
  ```
470
156
 
471
- ```javascript
472
-
473
- // Example usage of the OpenCV class
474
-
475
- import { OpenCV } from "node-native-win-utils";
476
-
477
-
478
-
479
- const image = new OpenCV("path/to/image.png");
480
-
481
-
482
-
483
- const template = new OpenCV("path/to/template.png");
484
-
485
- const matchedImage = image.matchTemplate(template.imageData);
486
-
487
-
488
-
489
- const blurredImage = image.blur(5, 5);
490
-
491
-
492
-
493
- const grayscaleImage = image.bgrToGray();
494
-
495
-
496
-
497
- const regionOfInterest = { x: 100, y: 100, width: 200, height: 150 };
498
-
499
- const regionImage = image.getRegion(regionOfInterest);
500
-
501
-
502
-
503
- const redColor = { r: 255, g: 0, b: 0 };
504
-
505
- const thickRectangle = image.drawRectangle(
506
-
507
- { x: 50, y: 50 },
508
-
509
- { x: 150, y: 150 },
510
-
511
- redColor,
512
-
513
- 3
514
-
515
- );
516
-
517
-
518
-
519
- matchedImage.imwrite("output/matched.png");
520
-
521
- blurredImage.imwrite("output/blurred.png");
522
-
523
- grayscaleImage.imwrite("output/grayscale.png");
524
-
525
- regionImage.imwrite("output/region.png");
526
-
527
- thickRectangle.imwrite("output/thick_rectangle.png");
528
-
157
+ #### Requires:
529
158
  ```
530
-
531
-
532
-
533
- ```javascript
534
-
535
- // If you want to aply blur and convert to gray then do it that order:
536
-
537
- image.blur(5, 5).bgrToGray();
538
-
539
- // Otherwise you will get an error.
540
-
159
+ Visual Studio Build Tools (C++).
160
+ Python 3
541
161
  ```
542
162
 
543
-
544
-
545
- Please note that the above example demonstrates the usage of different methods available in the `OpenCV` class. Make sure to replace `"path/to/image.png"` and `"path/to/template.png"` with actual image file paths.
546
-
547
-
548
-
549
- ```javascript
550
-
551
- import {
552
-
553
- getWindowData,
554
-
555
- captureWindow,
556
-
557
- mouseMove,
558
-
559
- mouseClick,
560
-
561
- mouseDrag,
562
-
563
- typeString,
564
-
565
- KeyListener,
566
-
567
- } from "node-native-win-utils";
568
-
569
-
570
-
571
- // Retrieve window data
572
-
573
-
574
-
575
- const windowData = getWindowData("My Window");
576
-
577
-
578
-
579
- console.log("Window data:", windowData);
580
-
581
-
582
-
583
- // Window data: { width: 1024, height: 768, x: 100, y: 100 }
584
-
585
-
586
-
587
- // Capture window screenshot
588
-
589
-
590
-
591
- captureWindow("My Window", "output.png");
592
-
593
-
594
-
595
- // Output: output.png with a screenshot of the window
596
-
597
-
598
-
599
- // Move the mouse
600
-
601
-
602
-
603
- mouseMove(100, 200);
604
-
605
-
606
-
607
- // Perform mouse click
608
-
609
-
610
-
611
- mouseClick(); // Left mouse click
612
-
613
-
614
-
615
- mouseClick("right"); // Right mouse click
616
-
617
-
618
-
619
- // Simulate mouse drag
620
-
621
-
622
-
623
- mouseDrag(100, 200, 300, 400);
624
-
625
-
626
-
627
- mouseDrag(100, 200, 300, 400, 100); // Drag with speed 100
628
-
629
-
630
-
631
- // Simulate typing
632
-
633
-
634
-
635
- typeString("Hello, world!");
636
-
637
-
638
-
639
- typeString("Hello, world!", 100); // Type with a delay of 100ms between characters
640
-
641
-
642
-
643
- // Use KeyboardListener class
644
-
645
-
646
-
647
- const listener = KeyboardListener.listener();
648
-
649
-
650
-
651
- listener.on("keyDown", (data) => {
652
-
653
- console.log("Key down:", data.keyCode, data.keyName);
654
-
655
- });
656
-
657
-
658
-
659
- // Key down: keyCode keyName
660
-
661
-
662
-
663
- listener.on("keyUp", (data) => {
664
-
665
- console.log("Key up:", data.keyCode, data.keyName);
666
-
667
- });
668
-
669
-
670
-
671
- // Key up: keyCode keyName
672
-
673
-
674
-
675
-
676
- ```
163
+ ## License
164
+ --
677
165
 
678
166
  [OpenCV License](https://github.com/opencv/opencv/blob/master/LICENSE)
167
+
679
168
  [MIT License](https://github.com/T-Rumibul/node-native-win-utils/blob/main/LICENSE)
680
169
 
681
170
 
682
- [license-src]: https://img.shields.io/github/license/nuxt-modules/icon.svg?style=for-the-badge&colorA=18181B&colorB=28CF8D
171
+ [license-src]: https://img.shields.io/badge/License-MIT-yellow.svg
683
172
 
684
173
  [license-href]: https://github.com/T-Rumibul/node-native-win-utils/blob/main/LICENSE
174
+
175
+
176
+ **Made with ❤️ for Windows automation.**
177
+ Issues / PRs welcome!
package/binding.gyp CHANGED
@@ -17,6 +17,7 @@
17
17
  "libraries": [
18
18
  "dwmapi.lib",
19
19
  "windowsapp.lib",
20
+ "Shcore.lib",
20
21
  "-lgdi32",
21
22
  "-lgdiplus",
22
23
  "<!(node -p \"require('path').resolve('libs/libjpeg-turbo.lib')\")",
package/dist/index.d.mts CHANGED
@@ -51,7 +51,7 @@ export type MouseMove = (posX: number, posY: number) => boolean;
51
51
  /**
52
52
  * Function type for simulating a mouse click.
53
53
  */
54
- export type MouseClick = (button?: "left" | "middle" | "right") => boolean;
54
+ export type MouseClick = (button?: "left" | "middle" | "right", type?: "down" | "up" | "click") => boolean;
55
55
  /**
56
56
  * Function type for simulating typing.
57
57
  */
@@ -80,11 +80,11 @@ export type StartMouseListener = (callback: (data: {
80
80
  y: number;
81
81
  type: string;
82
82
  }) => void) => void;
83
- export type StopMouseListener = (error: {
83
+ export type StopMouseListener = () => {
84
84
  success: boolean;
85
85
  error?: string;
86
86
  errorCode?: number;
87
- }) => void;
87
+ };
88
88
  export type MatchTemplate = (image: ImageData, template: ImageData, method?: number | null, mask?: ImageData) => MatchData;
89
89
  export type Blur = (image: ImageData, sizeX: number, sizeY: number) => ImageData;
90
90
  export type BgrToGray = (image: ImageData) => ImageData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-native-win-utils",
3
- "version": "2.2.0",
3
+ "version": "2.2.3",
4
4
  "author": "Andrew K.",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/T-Rumibul/node-native-win-utils.git",
@@ -40,7 +40,7 @@
40
40
  "install": "node-gyp-build",
41
41
  "build": "node-gyp clean && npx prebuildify --napi && node ./buildHelpers/dllCopy.js && npm run buildts",
42
42
  "buildts": "node ./buildHelpers/cjsCompatBuild.js",
43
- "test": "tap run "
43
+ "test": "tap run"
44
44
  },
45
45
  "dependencies": {
46
46
  "node-addon-api": "^8.6.0",
package/src/cpp/main.cpp CHANGED
@@ -6,10 +6,10 @@
6
6
  #include <mouse.cpp>
7
7
  #include <opencv.cpp>
8
8
  #include <tesseract.cpp>
9
-
9
+ #include <ShellScalingApi.h>
10
10
  Napi::Object Init(Napi::Env env, Napi::Object exports)
11
11
  {
12
-
12
+ SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
13
13
  exports.Set("getWindowData", Napi::Function::New(env, GetWindowData));
14
14
  exports.Set("captureWindowN", Napi::Function::New(env, CaptureWindow));
15
15
  exports.Set("captureScreenAsync", Napi::Function::New(env, CaptureScreenAsync));
package/src/cpp/mouse.cpp CHANGED
@@ -1,11 +1,36 @@
1
1
  // patrially used code from https://github.com/octalmage/robotjs witch is under MIT License Copyright (c) 2014 Jason Stallings
2
2
  #include <napi.h>
3
3
  #include <windows.h>
4
+ #include <atomic>
5
+ #include <cmath>
4
6
 
5
7
  HHOOK mouseHook;
6
8
  Napi::ThreadSafeFunction tsfn;
7
9
  static HANDLE hHookThread = NULL;
8
- static DWORD hookThreadId = 0;
10
+ static DWORD hookThreadId = 0;
11
+ static std::atomic_flag listenerRunning = ATOMIC_FLAG_INIT;
12
+ static std::atomic<bool> listenerStopping = false;
13
+ static HANDLE hHookReady = NULL;
14
+
15
+ struct Position
16
+ {
17
+ int x;
18
+ int y;
19
+ };
20
+
21
+ Position calcAbsolutePosition(int x, int y)
22
+ {
23
+ int vLeft = GetSystemMetrics(SM_XVIRTUALSCREEN);
24
+ int vTop = GetSystemMetrics(SM_YVIRTUALSCREEN);
25
+ int vWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN);
26
+ int vHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
27
+ if (vWidth <= 1 || vHeight <= 1)
28
+ return {0, 0}; // No virtual screen
29
+ int absoluteX = static_cast<int>((static_cast<long long>(x - vLeft) * 65535) / (vWidth - 1));
30
+ int absoluteY = static_cast<int>((static_cast<long long>(y - vTop) * 65535) / (vHeight - 1));
31
+ return {absoluteX, absoluteY};
32
+ }
33
+
9
34
  /**
10
35
  * Move the mouse to a specific point.
11
36
  * @param point The coordinates to move the mouse to (x, y).
@@ -24,13 +49,9 @@ Napi::Value MoveMouse(const Napi::CallbackInfo &info)
24
49
  int posX = info[0].As<Napi::Number>();
25
50
  int posY = info[1].As<Napi::Number>();
26
51
 
27
- // Get the screen metrics
28
- int screenWidth = GetSystemMetrics(SM_CXSCREEN);
29
- int screenHeight = GetSystemMetrics(SM_CYSCREEN);
30
-
31
- // Convert coordinates to absolute values
32
- int absoluteX = static_cast<int>((65536 * posX) / screenWidth);
33
- int absoluteY = static_cast<int>((65536 * posY) / screenHeight);
52
+ Position pos = calcAbsolutePosition(posX, posY);
53
+ int absoluteX = pos.x;
54
+ int absoluteY = pos.y;
34
55
 
35
56
  // Move the mouse
36
57
  INPUT mouseInput = {0};
@@ -39,50 +60,64 @@ Napi::Value MoveMouse(const Napi::CallbackInfo &info)
39
60
  mouseInput.mi.dy = absoluteY;
40
61
  mouseInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
41
62
  mouseInput.mi.time = 0; // System will provide the timestamp
42
-
43
- SendInput(1, &mouseInput, sizeof(mouseInput));
44
-
63
+ UINT sent = SendInput(1, &mouseInput, sizeof(INPUT));
64
+ if (sent == 0)
65
+ return Napi::Boolean::New(env, false);
45
66
  return Napi::Boolean::New(env, true);
46
67
  }
47
68
 
48
69
  Napi::Value ClickMouse(const Napi::CallbackInfo &info)
49
70
  {
50
71
  Napi::Env env = info.Env();
51
- std::string button;
52
-
53
- if (info.Length() < 1 || !info[0].IsString())
54
- button = "left";
55
- else
72
+ std::string button = "left";
73
+ std::string type = "click";
74
+ if (info.Length() >= 1 && info[0].IsString())
56
75
  button = info[0].As<Napi::String>();
76
+ if(info.Length() >= 2 && info[1].IsString())
77
+ type = info[1].As<Napi::String>();
78
+
57
79
 
58
- WORD mouseEvent = 0;
80
+ WORD downFlag = 0, upFlag = 0;
59
81
 
60
82
  if (button == "left")
61
83
  {
62
- mouseEvent = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP;
84
+ downFlag = MOUSEEVENTF_LEFTDOWN;
85
+ upFlag = MOUSEEVENTF_LEFTUP;
63
86
  }
64
87
  else if (button == "right")
65
88
  {
66
- mouseEvent = MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP;
89
+ downFlag = MOUSEEVENTF_RIGHTDOWN;
90
+ upFlag = MOUSEEVENTF_RIGHTUP;
67
91
  }
68
92
  else if (button == "middle")
69
93
  {
70
- mouseEvent = MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP;
94
+ downFlag = MOUSEEVENTF_MIDDLEDOWN;
95
+ upFlag = MOUSEEVENTF_MIDDLEUP;
71
96
  }
72
97
  else
73
98
  {
74
99
  Napi::TypeError::New(env, "Invalid button name").ThrowAsJavaScriptException();
75
100
  return env.Null();
76
101
  }
77
-
78
- // Perform the mouse click
79
- INPUT mouseInput = {0};
80
- mouseInput.type = INPUT_MOUSE;
81
- mouseInput.mi.dwFlags = mouseEvent;
82
- mouseInput.mi.time = 0; // System will provide the timestamp
83
-
84
- SendInput(1, &mouseInput, sizeof(mouseInput));
85
-
102
+ UINT sent = 0;
103
+ INPUT inputsClick[2] = {};
104
+ inputsClick[0].type = INPUT_MOUSE;
105
+ inputsClick[0].mi.dwFlags = downFlag;
106
+ inputsClick[1].type = INPUT_MOUSE;
107
+ inputsClick[1].mi.dwFlags = upFlag;
108
+ inputsClick[1].mi.time = 0;
109
+ inputsClick[0].mi.time = 0;
110
+ INPUT singleInput[1] = {};
111
+ singleInput[0].type = INPUT_MOUSE;
112
+ WORD flag = (type == "down") ? downFlag : upFlag;
113
+ singleInput[0].mi.dwFlags = flag;
114
+
115
+ if(type == "click")
116
+ sent = SendInput(2, inputsClick, sizeof(INPUT));
117
+ else
118
+ sent = SendInput(1, singleInput, sizeof(INPUT));
119
+ if (sent == 0)
120
+ return Napi::Boolean::New(env, false);
86
121
  return Napi::Boolean::New(env, true);
87
122
  }
88
123
 
@@ -95,7 +130,7 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
95
130
  Napi::TypeError::New(env, "You should provide startX, startY, endX, endY").ThrowAsJavaScriptException();
96
131
  return env.Null();
97
132
  }
98
-
133
+ bool success = true;
99
134
  int startX = info[0].As<Napi::Number>();
100
135
  int startY = info[1].As<Napi::Number>();
101
136
  int endX = info[2].As<Napi::Number>();
@@ -105,23 +140,26 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
105
140
  {
106
141
  speed = info[4].As<Napi::Number>();
107
142
  }
108
-
143
+ // Use pixel coords for timing
144
+ double pixelDistX = endX - startX;
145
+ double pixelDistY = endY - startY;
146
+ double pixelDistance = sqrt(pixelDistX * pixelDistX + pixelDistY * pixelDistY);
147
+ if (speed <= 0)
148
+ speed = 1; // guard div/0
149
+ double duration = pixelDistance / speed;
109
150
  // Get the screen metrics
110
- int screenWidth = GetSystemMetrics(SM_CXSCREEN);
111
- int screenHeight = GetSystemMetrics(SM_CYSCREEN);
112
151
 
152
+ Position startPos = calcAbsolutePosition(startX, startY);
153
+ Position endPos = calcAbsolutePosition(endX, endY);
113
154
  // Convert coordinates to absolute values
114
- int absoluteStartX = static_cast<int>((65536 * startX) / screenWidth);
115
- int absoluteStartY = static_cast<int>((65536 * startY) / screenHeight);
116
- int absoluteEndX = static_cast<int>((65536 * endX) / screenWidth);
117
- int absoluteEndY = static_cast<int>((65536 * endY) / screenHeight);
155
+ int absoluteStartX = startPos.x;
156
+ int absoluteStartY = startPos.y;
157
+ int absoluteEndX = endPos.x;
158
+ int absoluteEndY = endPos.y;
118
159
 
119
160
  // Calculate the distance and duration based on speed
120
161
  double distanceX = absoluteEndX - absoluteStartX;
121
162
  double distanceY = absoluteEndY - absoluteStartY;
122
- double distance = sqrt(distanceX * distanceX + distanceY * distanceY);
123
- double duration = distance / speed;
124
-
125
163
  // Move the mouse to the starting position
126
164
  INPUT startMouseInput = {0};
127
165
  startMouseInput.type = INPUT_MOUSE;
@@ -130,7 +168,9 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
130
168
  startMouseInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
131
169
  startMouseInput.mi.time = 0; // System will provide the timestamp
132
170
 
133
- SendInput(1, &startMouseInput, sizeof(startMouseInput));
171
+ if (SendInput(1, &startMouseInput, sizeof(INPUT)) == 0)
172
+ return Napi::Boolean::New(env, false);
173
+ ;
134
174
 
135
175
  // Perform mouse button down event
136
176
  INPUT mouseDownInput = {0};
@@ -138,7 +178,9 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
138
178
  mouseDownInput.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
139
179
  mouseDownInput.mi.time = 0; // System will provide the timestamp
140
180
 
141
- SendInput(1, &mouseDownInput, sizeof(mouseDownInput));
181
+ if (SendInput(1, &mouseDownInput, sizeof(INPUT)) == 0)
182
+ return Napi::Boolean::New(env, false);
183
+ ;
142
184
 
143
185
  // Calculate the number of steps based on the duration and desired speed
144
186
  const int steps = 100; // Adjust the number of steps for smoother movement
@@ -148,7 +190,7 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
148
190
  double stepY = distanceY / steps;
149
191
 
150
192
  // Move the mouse in increments to simulate dragging with speed control
151
- for (int i = 0; i < steps; ++i)
193
+ for (int i = 0; i <= steps; ++i)
152
194
  {
153
195
  // Calculate the position for the current step
154
196
  int currentX = static_cast<int>(absoluteStartX + (stepX * i));
@@ -162,10 +204,15 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
162
204
  mouseMoveInput.mi.dwFlags = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK;
163
205
  mouseMoveInput.mi.time = 0; // System will provide the timestamp
164
206
 
165
- SendInput(1, &mouseMoveInput, sizeof(mouseMoveInput));
207
+ if (SendInput(1, &mouseMoveInput, sizeof(INPUT)) == 0)
208
+ return Napi::Boolean::New(env, false);
209
+ ;
166
210
 
167
211
  // Sleep for a short duration to control the speed
168
- Sleep(static_cast<DWORD>(duration / steps));
212
+ if (i < steps)
213
+ {
214
+ Sleep(static_cast<DWORD>(duration / steps));
215
+ }
169
216
  }
170
217
 
171
218
  // Perform mouse button up event
@@ -174,15 +221,14 @@ Napi::Value DragMouse(const Napi::CallbackInfo &info)
174
221
  mouseUpInput.mi.dwFlags = MOUSEEVENTF_LEFTUP;
175
222
  mouseUpInput.mi.time = 0; // System will provide the timestamp
176
223
 
177
- SendInput(1, &mouseUpInput, sizeof(mouseUpInput));
178
-
179
- return Napi::Boolean::New(env, true);
224
+ if (SendInput(1, &mouseUpInput, sizeof(INPUT)) == 0)
225
+ return Napi::Boolean::New(env, false);
226
+ return Napi::Boolean::New(env, success);
180
227
  }
181
228
 
182
-
183
229
  LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
184
230
  {
185
- if (nCode >= 0)
231
+ if (nCode >= 0 && !listenerStopping.load())
186
232
  {
187
233
  MSLLHOOKSTRUCT *mouse = (MSLLHOOKSTRUCT *)lParam;
188
234
 
@@ -226,17 +272,23 @@ LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
226
272
  jsCallback.Call({event});
227
273
  };
228
274
 
229
- tsfn.BlockingCall(callback);
275
+ napi_status status = tsfn.NonBlockingCall(callback);
276
+ if (status != napi_ok)
277
+ {
278
+ // tsfn is already closing
279
+ return CallNextHookEx(mouseHook, nCode, wParam, lParam);
280
+ }
230
281
  }
231
282
 
232
283
  return CallNextHookEx(mouseHook, nCode, wParam, lParam);
233
284
  }
234
285
 
235
-
236
286
  DWORD WINAPI HookThread(LPVOID)
237
287
  {
238
288
  mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseProc, NULL, 0);
239
-
289
+ SetEvent(hHookReady);
290
+ if (mouseHook == NULL)
291
+ return 1;
240
292
  MSG msg;
241
293
  while (GetMessage(&msg, NULL, 0, 0))
242
294
  {
@@ -259,7 +311,7 @@ Napi::Value StartMouseListener(const Napi::CallbackInfo &info)
259
311
  return env.Null();
260
312
  }
261
313
  // Bug fix: prevent double-start leaking a thread and hook
262
- if (hHookThread != NULL)
314
+ if (listenerRunning.test_and_set())
263
315
  {
264
316
  Napi::TypeError::New(env, "Mouse listener already running").ThrowAsJavaScriptException();
265
317
  return env.Null();
@@ -274,12 +326,43 @@ Napi::Value StartMouseListener(const Napi::CallbackInfo &info)
274
326
  1);
275
327
 
276
328
  // Store handle and thread ID so StopMouseListener can signal and wait
329
+ hHookReady = CreateEvent(NULL, TRUE, FALSE, NULL);
330
+ if (hHookReady == NULL)
331
+ {
332
+ tsfn.Release();
333
+ listenerRunning.clear();
334
+ Napi::Error::New(env, "Failed to create sync event").ThrowAsJavaScriptException();
335
+ return env.Null();
336
+ }
277
337
  hHookThread = CreateThread(NULL, 0, HookThread, NULL, 0, &hookThreadId);
278
-
338
+ if (hHookThread == NULL)
339
+ {
340
+ tsfn.Release();
341
+ CloseHandle(hHookReady);
342
+ hHookReady = NULL;
343
+ listenerRunning.clear(); // release the guard so caller can retry
344
+ Napi::Error::New(env, "Failed to create hook thread").ThrowAsJavaScriptException();
345
+ return env.Null();
346
+ }
347
+ WaitForSingleObject(hHookReady, 2000);
348
+ CloseHandle(hHookReady);
349
+ hHookReady = NULL;
350
+ DWORD exitCode = 0;
351
+ GetExitCodeThread(hHookThread, &exitCode);
352
+ if (exitCode == 1)
353
+ {
354
+ WaitForSingleObject(hHookThread, 1000);
355
+ CloseHandle(hHookThread);
356
+ hHookThread = NULL;
357
+ hookThreadId = 0;
358
+ tsfn.Release();
359
+ listenerRunning.clear();
360
+ Napi::Error::New(env, "Failed to install mouse hook").ThrowAsJavaScriptException();
361
+ return env.Null();
362
+ }
279
363
  return Napi::Boolean::New(env, true);
280
364
  }
281
365
 
282
-
283
366
  Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
284
367
  {
285
368
  Napi::Env env = info.Env();
@@ -292,10 +375,13 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
292
375
  return result;
293
376
  }
294
377
 
378
+ listenerStopping.store(true);
295
379
  // PostThreadMessage with WM_QUIT breaks the GetMessage loop in HookThread,
296
380
  // which then runs UnhookWindowsHookEx and exits
297
381
  if (!PostThreadMessage(hookThreadId, WM_QUIT, 0, 0))
298
382
  {
383
+ listenerStopping.store(false);
384
+ listenerRunning.clear();
299
385
  result.Set("success", false);
300
386
  result.Set("error", "Failed to signal hook thread");
301
387
  result.Set("errorCode", Napi::Number::New(env, GetLastError()));
@@ -306,8 +392,11 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
306
392
  DWORD waitResult = WaitForSingleObject(hHookThread, 3000);
307
393
  if (waitResult != WAIT_OBJECT_0)
308
394
  {
309
- // Thread didn't exit in time force it and warn
395
+ // Thread didn't exit in time - kill it
310
396
  TerminateThread(hHookThread, 1);
397
+ // Best-effort: reduce chance a concurrent NonBlockingCall is still
398
+ // in-flight before tsfn.Release() below. Not a hard guarantee.
399
+ Sleep(50);
311
400
  result.Set("success", false);
312
401
  result.Set("error", "Hook thread did not exit cleanly, was forcefully terminated");
313
402
  }
@@ -322,6 +411,7 @@ Napi::Value StopMouseListener(const Napi::CallbackInfo &info)
322
411
  CloseHandle(hHookThread);
323
412
  hHookThread = NULL;
324
413
  hookThreadId = 0;
325
-
414
+ listenerRunning.clear();
415
+ listenerStopping.store(false);
326
416
  return result;
327
417
  }
package/src/index.mts CHANGED
@@ -68,7 +68,7 @@ export type MouseMove = (posX: number, posY: number) => boolean;
68
68
  /**
69
69
  * Function type for simulating a mouse click.
70
70
  */
71
- export type MouseClick = (button?: "left" | "middle" | "right") => boolean;
71
+ export type MouseClick = (button?: "left" | "middle" | "right", type?: "down" | "up" | "click") => boolean;
72
72
 
73
73
  /**
74
74
  * Function type for simulating typing.