diff --git a/.gitignore b/.gitignore index 3e2e84b..34b23da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -build/ -node_modules/ +/build/ +/node_modules/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/README.md b/README.md deleted file mode 100644 index 3abe386..0000000 --- a/README.md +++ /dev/null @@ -1,40 +0,0 @@ - -# `capture-window` - -Capture a desktop window to a png file. Simple and easy. - -## Installation - -```sh -npm install --save capture-window -``` - -## Usage - -```javascript -var captureWindow = require('capture-window'); - -captureWindow('Finder', 'Downloads', function (err, filePath) { - if (err) { throw err; } - - // filePath is the path to a png file -}); -``` - -## API - -### `captureWindow(bundle, title[, filePath], cb)` - -Captures the window with the title `title` of type `bundle`. `bundle` is usually the name -of the application, e.g. `Finder`, `Safari`, `Terminal`. - -The callback receives `(err, filePath)`. - -## OS Support - -Only `Mac OS X` at the time being. The source is well prepared for other -systems, pull requests welcome. - -## License - -MIT diff --git a/binding.gyp b/binding.gyp index 5d480ae..64e4695 100644 --- a/binding.gyp +++ b/binding.gyp @@ -1,15 +1,12 @@ { "targets": [{ - "target_name": "capture-window", - "sources": [ "src/capture-window.cc" ], - "include_dirs" : [ - " +export = captureWindow diff --git a/index.js b/index.js index 61eab0c..e2b0d9e 100644 --- a/index.js +++ b/index.js @@ -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) } diff --git a/package.json b/package.json index d953f79..4aea8ca 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,20 @@ "name": "capture-window", "version": "0.1.3", "license": "MIT", - "author": "Linus Unnebäck ", "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" } } diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..88ba944 --- /dev/null +++ b/readme.md @@ -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` - 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 diff --git a/src/apple.m b/src/apple.m index 781f1e3..657dcad 100644 --- a/src/apple.m +++ b/src/apple.m @@ -1,14 +1,40 @@ - #include #include -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); @@ -23,15 +49,17 @@ } 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); @@ -39,7 +67,100 @@ 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; } diff --git a/src/base.cc b/src/base.cc deleted file mode 100644 index d827a10..0000000 --- a/src/base.cc +++ /dev/null @@ -1,35 +0,0 @@ - -class CaptureWindowWorker : public Nan::AsyncWorker { -public: - CaptureWindowWorker (v8::Local bundle, v8::Local title, v8::Local path, Nan::Callback *callback) - : Nan::AsyncWorker(callback), bundle(bundle), title(title), path(path) {} - ~CaptureWindowWorker () {} - - void Execute (); - - void HandleOKCallback () { - v8::Local argv[] = { - Nan::Null(), - Nan::New(*path).ToLocalChecked() - }; - - callback->Call(2, argv); - } - -private: - Nan::Utf8String bundle; - Nan::Utf8String title; - Nan::Utf8String path; -}; - -NAN_METHOD(capture_window) { - Nan::Callback *cb = new Nan::Callback(info[3].As()); - Nan::AsyncQueueWorker(new CaptureWindowWorker(info[0], info[1], info[2], cb)); -} - -NAN_MODULE_INIT(Initialize) { - Nan::Set(target, Nan::New("captureWindow").ToLocalChecked(), - Nan::GetFunction(Nan::New(capture_window)).ToLocalChecked()); -} - -NODE_MODULE(capture_window, Initialize) diff --git a/src/capture-window.c b/src/capture-window.c new file mode 100644 index 0000000..84a5a7e --- /dev/null +++ b/src/capture-window.c @@ -0,0 +1,23 @@ +#include + +#define NAPI_VERSION 1 +#include + +#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) diff --git a/src/capture-window.cc b/src/capture-window.cc deleted file mode 100644 index 2b2f07a..0000000 --- a/src/capture-window.cc +++ /dev/null @@ -1,11 +0,0 @@ - -#include -#include - -#include "base.cc" - -#ifdef __APPLE__ -#include "apple.m" -#else -#error Platform not supported -#endif diff --git a/test/simple.js b/test/simple.js index 7e3a4b2..eb1da7d 100644 --- a/test/simple.js +++ b/test/simple.js @@ -1,37 +1,30 @@ /* eslint-env mocha */ -var fs = require('fs') -var assert = require('assert') -var captureWindow = require('../') +const fs = require('fs') +const assert = require('assert') +const captureWindow = require('../') describe('captureWindow', function () { - var cleanup = [] + let cleanup = [] - afterEach(function () { - cleanup.forEach(fs.unlinkSync.bind(fs)) + afterEach(async () => { + for (const item of cleanup) fs.unlinkSync(item) cleanup = [] }) - it('captures a window', function (done) { - captureWindow('Window Server', 'Menubar', function (err, filePath) { - assert.ifError(err) + it('captures a window', async () => { + const filePath = await captureWindow('Window Server', 'Menubar') + cleanup.push(filePath) - cleanup.push(filePath) + const data = fs.readFileSync(filePath) - fs.readFile(filePath, function (err, data) { - assert.ifError(err) - - assert.equal(data[0], 0x89) - assert.equal(data[1], 0x50) - assert.equal(data[2], 0x4E) - assert.equal(data[3], 0x47) - assert.equal(data[4], 0x0D) - assert.equal(data[5], 0x0A) - assert.equal(data[6], 0x1A) - assert.equal(data[7], 0x0A) - - done() - }) - }) + assert.equal(data[0], 0x89) + assert.equal(data[1], 0x50) + assert.equal(data[2], 0x4E) + assert.equal(data[3], 0x47) + assert.equal(data[4], 0x0D) + assert.equal(data[5], 0x0A) + assert.equal(data[6], 0x1A) + assert.equal(data[7], 0x0A) }) })