Skip to content

Commit 9cae49d

Browse files
committed
feat: add screenshot options
1 parent 39d2c75 commit 9cae49d

File tree

4 files changed

+108
-8
lines changed

4 files changed

+108
-8
lines changed

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,18 @@ console.log(display)
139139
*/
140140
```
141141

142-
### `displays.screenshot(id)`
142+
### `displays.screenshot(id, options)`
143143

144144
* `id` Number - The device ID for the display.
145+
* `options` Object
146+
* `type` String - The representation of the image. Can be 'jpeg', 'png', or 'tiff'. Defaults to 'jpeg'.
147+
* `bounds` Object
148+
* `x` Number - The x coordinate of the origin of the rectangle.
149+
* `y` Number - The y coordinate of the origin of the rectangle.
150+
* `width` Number - The width of the rectangle.
151+
* `height` Number - The height of the rectangle.
152+
153+
Returns `Buffer` - a Buffer representation of the desired display screenshot.
145154

146155
Takes a screenshot of the display with the specified id.
147156

@@ -154,7 +163,14 @@ const path = require('path')
154163
const { id } = displays.getPrimaryDisplay()
155164

156165
const ssPath = path.resolve(__dirname, 'screenshot.jpg')
157-
const screenshotData = display.screenshot(id)
166+
const screenshotData = displays.screenshot(id, {
167+
bounds: {
168+
x: 100,
169+
y: 500,
170+
width: 200,
171+
height: 200
172+
}
173+
})
158174

159175
// Write out JPEG image as screenshot.jpg in the current directory.
160176
fs.writeFileSync(ssPath, screenshotData)

displays.mm

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@
3030
return obj;
3131
}
3232

33+
// Converts a bounds object to an NSRect.
34+
NSRect BoundsObjectToNSRect(Napi::Object bounds) {
35+
int64_t x = bounds.Get("x").As<Napi::Number>().Int64Value();
36+
int64_t y = bounds.Get("y").As<Napi::Number>().Int64Value();
37+
int64_t width = bounds.Get("width").As<Napi::Number>().Int64Value();
38+
int64_t height = bounds.Get("height").As<Napi::Number>().Int64Value();
39+
40+
return NSMakeRect((CGFloat)x, (CGFloat)y, (CGFloat)width, (CGFloat)height);
41+
}
42+
3343
// Converts an NSColorSpace to an object containing information about the
3444
// colorSpace.
3545
Napi::Object NSColorSpaceToObject(Napi::Env env, NSColorSpace *color_space) {
@@ -113,21 +123,41 @@ bool GetIsMonochrome() {
113123
return displays;
114124
}
115125

126+
// Takes a screenshot of the specified display.
116127
Napi::Buffer<uint8_t> Screenshot(const Napi::CallbackInfo &info) {
117128
Napi::Env env = info.Env();
129+
118130
uint32_t display_id = info[0].As<Napi::Number>().Uint32Value();
131+
Napi::Object options = info[1].As<Napi::Object>();
132+
133+
NSBitmapImageFileType image_type = NSBitmapImageFileTypeJPEG;
134+
if (options.Has("type")) {
135+
std::string type = options.Get("type").As<Napi::String>().Utf8Value();
136+
if (type == "png") {
137+
image_type = NSBitmapImageFileTypePNG;
138+
} else if (type == "tiff") {
139+
image_type = NSBitmapImageFileTypeTIFF;
140+
}
141+
}
142+
143+
CGImageRef img_ref;
144+
if (options.Has("bounds")) {
145+
Napi::Object bounds = options.Get("bounds").As<Napi::Object>();
146+
CGRect rect = NSRectToCGRect(BoundsObjectToNSRect(bounds));
147+
img_ref = CGDisplayCreateImageForRect(display_id, rect);
148+
} else {
149+
img_ref = CGDisplayCreateImage(display_id);
150+
}
119151

120-
CGImageRef img_ref = CGDisplayCreateImage(display_id);
121152
if (!img_ref)
122153
return Napi::Buffer<uint8_t>::New(env, 0);
123154

124155
std::vector<uint8_t> data;
125156

126157
NSBitmapImageRep *bitmap_rep =
127158
[[NSBitmapImageRep alloc] initWithCGImage:img_ref];
128-
NSData *image_data =
129-
[bitmap_rep representationUsingType:NSBitmapImageFileTypeJPEG
130-
properties:@{}];
159+
NSData *image_data = [bitmap_rep representationUsingType:image_type
160+
properties:@{}];
131161
const uint8 *bytes = (uint8 *)[image_data bytes];
132162
data.assign(bytes, bytes + [image_data length]);
133163

index.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,35 @@ function getDisplayFromID(id) {
88
return displays.getDisplayFromID.call(this, id)
99
}
1010

11-
function screenshot(id) {
11+
function screenshot(id, options = {}) {
1212
if (typeof id !== 'number') {
1313
throw new TypeError(`'id' must be a number`)
1414
}
1515

16-
return displays.screenshot.call(this, id)
16+
const validTypes = ['jpeg', 'tiff', 'png']
17+
if (options.type) {
18+
if (typeof options.type !== 'string') {
19+
throw new Error(`'type' must be a string`)
20+
} else if (!validTypes.includes(options.type)) {
21+
throw new Error(`'type' must be one of ${validTypes.join(', ')}`)
22+
}
23+
}
24+
25+
if (options.bounds) {
26+
if (typeof options.bounds !== 'object') {
27+
throw new Error(`'bounds' must be a number`)
28+
} else if (!options.bounds.x || typeof options.bounds.x !== 'number') {
29+
throw new Error(`'bounds.x' must be a number`)
30+
} else if (!options.bounds.y || typeof options.bounds.y !== 'number') {
31+
throw new Error(`'bounds.y' must be a number`)
32+
} else if (!options.bounds.width || typeof options.bounds.width !== 'number') {
33+
throw new Error(`'bounds.width' must be a number`)
34+
} else if (!options.bounds.height || typeof options.bounds.height !== 'number') {
35+
throw new Error(`'bounds.height' must be a number`)
36+
}
37+
}
38+
39+
return displays.screenshot.call(this, id, options)
1740
}
1841

1942
module.exports = {

test/module.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,37 @@ describe('node-system-displays', () => {
126126
}).to.throw(`'id' must be a number`)
127127
})
128128

129+
it('throws an error if options.type is not valid', () => {
130+
expect(() => {
131+
const { id } = getPrimaryDisplay()
132+
screenshot(id, { type: 'bad-type' })
133+
}).to.throw(`'type' must be one of jpeg, tiff, png`)
134+
})
135+
136+
it('throws an error if options.bounds are not valid', () => {
137+
const { id } = getPrimaryDisplay()
138+
139+
expect(() => {
140+
screenshot(id, { bounds: 'oh no' })
141+
}).to.throw(`'bounds' must be a number`)
142+
143+
expect(() => {
144+
screenshot(id, { bounds: { x: 'bad', y: 1, width: 10, height: 10 } })
145+
}).to.throw(`'bounds.x' must be a number`)
146+
147+
expect(() => {
148+
screenshot(id, { bounds: { x: 1, y: 'bad', width: 10, height: 10 } })
149+
}).to.throw(`'bounds.y' must be a number`)
150+
151+
expect(() => {
152+
screenshot(id, { bounds: { x: 1, y: 1, width: 'bad', height: 10 } })
153+
}).to.throw(`'bounds.width' must be a number`)
154+
155+
expect(() => {
156+
screenshot(id, { bounds: { x: 1, y: 1, width: 10, height: 'bad' } })
157+
}).to.throw(`'bounds.height' must be a number`)
158+
})
159+
129160
it('can write out a screenshot to the current directory', () => {
130161
const { id } = getPrimaryDisplay()
131162

0 commit comments

Comments
 (0)