Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <linus@folkdatorn.se>",
"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