Skip to content

Commit

Permalink
Merge pull request #4 from LinusU/lu-1
Browse files Browse the repository at this point in the history
💥 Use Node-API to build addon
  • Loading branch information
LinusU authored Nov 7, 2022
2 parents 4533771 + ac50a10 commit b36d053
Show file tree
Hide file tree
Showing 13 changed files with 236 additions and 154 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
build/
node_modules/
/build/
/node_modules/
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
40 changes: 0 additions & 40 deletions README.md

This file was deleted.

11 changes: 4 additions & 7 deletions binding.gyp
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
{
"targets": [{
"target_name": "capture-window",
"sources": [ "src/capture-window.cc" ],
"include_dirs" : [
"<!(node -e \"require('nan')\")"
],
"target_name": "capture_window",
"sources": [ "src/capture-window.c" ],
"conditions": [
["OS==\"mac\"", {
"libraries": [ "-framework Foundation" ],
"libraries": [ "-framework Cocoa", "-framework CoreGraphics" ],
"xcode_settings": {
"OTHER_CFLAGS": [ "-ObjC++" ]
"OTHER_CFLAGS": [ "-ObjC" ]
}
}]
]
Expand Down
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Captures the window with the title `title` of type `bundle`. `bundle` is usually the name of the application, e.g. `Finder`, `Safari`, `Terminal`.
*
* @returns path to a png file
*/
declare async function captureWindow (bundle: string, title: string, filePath?: string | null): Promise<string>
export = captureWindow
21 changes: 5 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
var temp = require('fs-temp').template('%s.png')
var addon = require('./build/Release/capture-window')
const temp = require('fs-temp/promise').template('%s.png')
const addon = require('./build/Release/capture_window')

module.exports = function captureWindow (bundle, title, filePath, cb) {
var done = (typeof filePath === 'function' ? filePath : cb)
var hasPath = (typeof filePath === 'string')
module.exports = async function captureWindow (bundle, title, filePath) {
if (filePath == null) filePath = await temp.writeFile('')

function withPath (err, filePath) {
if (err) return done(err)

addon.captureWindow(bundle, title, filePath, done)
}

if (hasPath) {
withPath(null, filePath)
} else {
temp.writeFile('', withPath)
}
return addon.capture(bundle, title, filePath)
}
19 changes: 9 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@
"name": "capture-window",
"version": "0.1.3",
"license": "MIT",
"author": "Linus Unnebäck <[email protected]>",
"main": "index.js",
"scripts": {
"test": "standard && mocha"
},
"repository": {
"type": "git",
"url": "http://github.com/LinusU/node-capture-window.git"
"test": "standard && mocha && ts-readme-generator --check"
},
"repository": "LinusU/node-capture-window",
"dependencies": {
"fs-temp": "^0.1.1",
"nan": "^2.0.5"
"fs-temp": "^1.2.1"
},
"devDependencies": {
"mocha": "^2.1.0",
"standard": "^4.3.1"
"mocha": "^7.2.0",
"standard": "^15.0.1",
"ts-readme-generator": "^0.7.3"
},
"engines": {
"node": ">=8.10.0"
}
}
38 changes: 38 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `capture-window`

Capture a desktop window to a png file. Simple and easy.

## Installation

```sh
npm install --save capture-window
```

## Usage

```javascript
const captureWindow = require('capture-window')

captureWindow('Finder', 'Downloads').then((filePath) => {
// filePath is the path to a png file
})
```

## API

### `captureWindow(bundle, title[, filePath])`

- `bundle` (`string`, required)
- `title` (`string`, required)
- `filePath` (`string | null`, optional)
- returns `Promise<string>` - path to a png file

Captures the window with the title `title` of type `bundle`. `bundle` is usually the name of the application, e.g. `Finder`, `Safari`, `Terminal`.

## OS Support

Only `Mac OS X` at the time being. The source is well prepared for other systems, pull requests welcome.

## License

MIT
137 changes: 129 additions & 8 deletions src/apple.m
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@

#include <Cocoa/Cocoa.h>
#include <CoreGraphics/CGWindow.h>

void CaptureWindowWorker::Execute () {
// Failed to find window
#define ERROR_FAILED_TO_FIND_WINDOW 1
// Failed to create CGImageDestination
#define ERROR_FAILED_TO_CREATE_CG_IMAGE_DESTINATION 2
// Failed to write image
#define ERROR_FAILED_TO_WRITE_IMAGE 3
// No access to screen capture
#define ERROR_NO_ACCESS_TO_SCREEN_CAPTURE 4

typedef struct {
char* bundle;
char* title;
char* filename;
napi_deferred deferred;
int error_code;
} CaptureData;

void capture_execute(napi_env env, void* _data) {
CaptureData* data = (CaptureData *) _data;

if (@available(macOS 10.15, *)) {
if (!CGPreflightScreenCaptureAccess()) {
if (!CGRequestScreenCaptureAccess()) {
data->error_code = ERROR_NO_ACCESS_TO_SCREEN_CAPTURE;
return;
}
}
}

uint32_t windowId;
bool foundWindow = false;

NSString *nsBundle = [NSString stringWithUTF8String: *bundle];
NSString *nsTitle = [NSString stringWithUTF8String: *title];
NSString *nsBundle = [NSString stringWithUTF8String: data->bundle];
NSString *nsTitle = [NSString stringWithUTF8String: data->title];

NSArray *windows = (NSArray *) CGWindowListCopyWindowInfo(kCGWindowListExcludeDesktopElements,kCGNullWindowID);

Expand All @@ -23,23 +49,118 @@
}

if (foundWindow == false) {
return SetErrorMessage("Failed to find window");
data->error_code = ERROR_FAILED_TO_FIND_WINDOW;
return;
}

CGImageRef img = CGWindowListCreateImage(CGRectNull, kCGWindowListOptionIncludingWindow, windowId, kCGWindowImageBoundsIgnoreFraming);
CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:[NSString stringWithUTF8String: *path]];
CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:[NSString stringWithUTF8String: data->filename]];
CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, NULL);

if (!destination) {
return SetErrorMessage("Failed to create CGImageDestination");
data->error_code = ERROR_FAILED_TO_CREATE_CG_IMAGE_DESTINATION;
return;
}

CGImageDestinationAddImage(destination, img, nil);
bool success = CGImageDestinationFinalize(destination);
CFRelease(destination);

if (!success) {
return SetErrorMessage("Failed to write image");
data->error_code = ERROR_FAILED_TO_WRITE_IMAGE;
return;
}
}

void capture_complete(napi_env env, napi_status status, void* _data) {
CaptureData* data = (CaptureData *) _data;

switch (data->error_code) {
case ERROR_FAILED_TO_FIND_WINDOW: {
napi_value error;
napi_value error_msg;
assert(napi_create_string_utf8(env, "Failed to find window", NAPI_AUTO_LENGTH, &error_msg) == napi_ok);
assert(napi_create_error(env, NULL, error_msg, &error) == napi_ok);
assert(napi_reject_deferred(env, data->deferred, error) == napi_ok);
goto cleanup;
}

case ERROR_FAILED_TO_CREATE_CG_IMAGE_DESTINATION: {
napi_value error;
napi_value error_msg;
assert(napi_create_string_utf8(env, "Failed to create CGImageDestination", NAPI_AUTO_LENGTH, &error_msg) == napi_ok);
assert(napi_create_error(env, NULL, error_msg, &error) == napi_ok);
assert(napi_reject_deferred(env, data->deferred, error) == napi_ok);
goto cleanup;
}

case ERROR_FAILED_TO_WRITE_IMAGE: {
napi_value error;
napi_value error_msg;
assert(napi_create_string_utf8(env, "Failed to write image", NAPI_AUTO_LENGTH, &error_msg) == napi_ok);
assert(napi_create_error(env, NULL, error_msg, &error) == napi_ok);
assert(napi_reject_deferred(env, data->deferred, error) == napi_ok);
goto cleanup;
}

case ERROR_NO_ACCESS_TO_SCREEN_CAPTURE: {
napi_value error;
napi_value error_msg;
assert(napi_create_string_utf8(env, "No access to screen capture", NAPI_AUTO_LENGTH, &error_msg) == napi_ok);
assert(napi_create_error(env, NULL, error_msg, &error) == napi_ok);
assert(napi_reject_deferred(env, data->deferred, error) == napi_ok);
goto cleanup;
}

case 0: break;
default: assert(false);
}

napi_value result;
assert(napi_create_string_utf8(env, data->filename, NAPI_AUTO_LENGTH, &result) == napi_ok);
assert(napi_resolve_deferred(env, data->deferred, result) == napi_ok);

cleanup:
free(data->bundle);
free(data->title);
free(data->filename);
free(_data);
}

napi_value capture(napi_env env, napi_callback_info info) {
size_t argc = 3;
napi_value args[3];
assert(napi_get_cb_info(env, info, &argc, args, NULL, NULL) == napi_ok);

CaptureData* data = (CaptureData *) malloc(sizeof(CaptureData));

data->error_code = 0;

size_t bundle_length;
assert(napi_get_value_string_utf8(env, args[0], NULL, 0, &bundle_length) == napi_ok);
data->bundle = (char *) malloc(bundle_length + 1);
assert(napi_get_value_string_utf8(env, args[0], data->bundle, bundle_length + 1, NULL) == napi_ok);

size_t title_length;
assert(napi_get_value_string_utf8(env, args[1], NULL, 0, &title_length) == napi_ok);
data->title = (char *) malloc(title_length + 1);
assert(napi_get_value_string_utf8(env, args[1], data->title, title_length + 1, NULL) == napi_ok);

size_t filename_length;
assert(napi_get_value_string_utf8(env, args[2], NULL, 0, &filename_length) == napi_ok);
data->filename = (char *) malloc(filename_length + 1);
assert(napi_get_value_string_utf8(env, args[2], data->filename, filename_length + 1, NULL) == napi_ok);

napi_value promise;
assert(napi_create_promise(env, &data->deferred, &promise) == napi_ok);

napi_value work_name;
assert(napi_create_string_utf8(env, "capture-window", NAPI_AUTO_LENGTH, &work_name) == napi_ok);

napi_async_work work;
assert(napi_create_async_work(env, NULL, work_name, capture_execute, capture_complete, (void*) data, &work) == napi_ok);

assert(napi_queue_async_work(env, work) == napi_ok);

return promise;
}
35 changes: 0 additions & 35 deletions src/base.cc

This file was deleted.

23 changes: 23 additions & 0 deletions src/capture-window.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include <assert.h>

#define NAPI_VERSION 1
#include <node_api.h>

#ifdef __APPLE__
#include "apple.m"
#else
#error Platform not supported
#endif

static napi_value Init(napi_env env, napi_value exports) {
napi_value result;
assert(napi_create_object(env, &result) == napi_ok);

napi_value capture_fn;
assert(napi_create_function(env, "capture", NAPI_AUTO_LENGTH, capture, NULL, &capture_fn) == napi_ok);
assert(napi_set_named_property(env, result, "capture", capture_fn) == napi_ok);

return result;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
Loading

0 comments on commit b36d053

Please sign in to comment.