Skip to content
Draft
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
17 changes: 17 additions & 0 deletions packages/electron-screenshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,20 @@ screenshots.startCapture();
## Screenshot

![screenshot](../../screenshot.jpg)

## Long screenshot example

`LongCapture` helps to take scrolling screenshots. It periodically captures the
screen and stitches the frames by template matching.

```ts
import Screenshots, { LongCapture } from 'electron-screenshots'

const screenshots = new Screenshots()
const long = new LongCapture(screenshots, { interval: 800 })

await long.start()
// user scrolls the page slowly...
const buffer = long.stop()
require('fs').writeFileSync('long.png', buffer)
```
4 changes: 3 additions & 1 deletion packages/electron-screenshots/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"debug": "^4.3.4",
"fs-extra": "^11.1.1",
"node-screenshots": "^0.2.1",
"react-screenshots": "^0.5.22"
"react-screenshots": "^0.5.22",
"pngjs": "^7.0.0",
"uiohook-napi": "^0.6.2"
},
"peerDependencies": {
"electron": ">=14"
Expand Down
13 changes: 12 additions & 1 deletion packages/electron-screenshots/src/demo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-console */
import { app, BrowserWindow, globalShortcut } from 'electron';
import Screenshots from '.';
import fs from 'fs-extra';
import Screenshots, { LongCapture } from '.';

app.whenReady().then(() => {
const screenshots = new Screenshots({
Expand All @@ -9,12 +10,22 @@ app.whenReady().then(() => {
},
singleWindow: true,
});
const long = new LongCapture(screenshots, { interval: 800 });
screenshots.$view.webContents.openDevTools();

globalShortcut.register('ctrl+shift+a', () => {
screenshots.startCapture();
});

globalShortcut.register('ctrl+shift+l', async () => {
await long.start();
});

globalShortcut.register('ctrl+shift+s', () => {
const buffer = long.stop();
fs.writeFileSync('long.png', buffer);
});

screenshots.on('windowCreated', ($win) => {
$win.on('focus', () => {
globalShortcut.register('esc', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/electron-screenshots/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Event from './event';
import getDisplay, { Display } from './getDisplay';
import padStart from './padStart';
import { Bounds, ScreenshotsData } from './preload';
export { default as LongCapture } from './longCapture';

export type LoggerFn = (...args: unknown[]) => void;
export type Logger = Debugger | LoggerFn;
Expand Down
100 changes: 100 additions & 0 deletions packages/electron-screenshots/src/longCapture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { EventEmitter } from 'events';
import { PNG } from 'pngjs';
import Screenshots from './index';
import getDisplay from './getDisplay';

interface LongCaptureOptions {
interval?: number; // capture interval in ms
maxOffset?: number; // template match search range
}

export default class LongCapture extends EventEmitter {
private screenshots: Screenshots;
private options: Required<LongCaptureOptions>;
private timer: NodeJS.Timeout | null = null;
private frames: PNG[] = [];

constructor(screenshots: Screenshots, options?: LongCaptureOptions) {
super();
this.screenshots = screenshots;
this.options = {
interval: 1000,
maxOffset: 200,
...options,
} as Required<LongCaptureOptions>;
}

/** Start listening scroll capture */
public async start() {
const display = getDisplay();
const dataURL = await (this.screenshots as any).capture(display);
this.frames.push(this.loadPNG(dataURL));
this.timer = setInterval(async () => {
const url = await (this.screenshots as any).capture(display);
this.frames.push(this.loadPNG(url));
}, this.options.interval);
}

/** Stop capture and return stitched image buffer */
public stop(): Buffer {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
const stitched = this.stitchFrames();
const buffer = PNG.sync.write(stitched);
this.frames = [];
return buffer;
}

private loadPNG(dataURL: string): PNG {
const b64 = dataURL.replace(/^data:image\/png;base64,/, '');
const buffer = Buffer.from(b64, 'base64');
return PNG.sync.read(buffer);
}

private verticalOffset(a: PNG, b: PNG): number {
const max = Math.min(this.options.maxOffset, b.height);
let bestOffset = 0;
let bestDiff = Number.POSITIVE_INFINITY;
for (let off = 0; off <= max; off++) {
let diff = 0;
for (let y = 0; y < b.height - off; y++) {
for (let x = 0; x < b.width; x++) {
const idxA = ((off + y) * a.width + x) << 2;
const idxB = (y * b.width + x) << 2;
diff += Math.abs(a.data[idxA] - b.data[idxB]);
diff += Math.abs(a.data[idxA + 1] - b.data[idxB + 1]);
diff += Math.abs(a.data[idxA + 2] - b.data[idxB + 2]);
}
}
if (diff < bestDiff) {
bestDiff = diff;
bestOffset = off;
}
}
return bestOffset;
}

private stitchFrames(): PNG {
if (this.frames.length === 0) {
throw new Error('no frames captured');
}
let output = this.frames[0];
let totalHeight = output.height;
for (let i = 1; i < this.frames.length; i++) {
const prev = output;
const curr = this.frames[i];
const offset = this.verticalOffset(prev, curr);
const merged = new PNG({ width: prev.width, height: totalHeight + curr.height - offset });
// copy previous image
PNG.bitblt(prev, merged, 0, 0, prev.width, totalHeight, 0, 0);
// copy non-overlap of current
PNG.bitblt(curr, merged, 0, offset, curr.width, curr.height - offset, 0, totalHeight);
totalHeight += curr.height - offset;
output = merged;
}
return output;
}
}