Skip to content

Commit

Permalink
Render "offscreen" to screenshot with paint events
Browse files Browse the repository at this point in the history
Sometime mid-year, Electron added support for "offscreen" rendering (it still shows a window, but the whole rendering pipeline is a bit different). When this mode is enabled, the rendered view can be explicitly invalidated, which is *much* better and more reliable than the way Nightmare currently tries to force new frames to render by fiddling around with the DOM (see issues segment-boneyard#555, segment-boneyard#736, segment-boneyard#809).

This isn't without its downsides; it doesn't work with forced device scale factors and it renders differently than with native system rendering (e.g. text rendering will be slightly different).
  • Loading branch information
Mr0grog committed Dec 16, 2016
1 parent ad8058a commit 53dee8a
Showing 1 changed file with 53 additions and 8 deletions.
61 changes: 53 additions & 8 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,18 @@ app.on('ready', function() {
preload: join(__dirname, 'preload.js'),
nodeIntegration: false
}
})
});

// If possible, turn on offscreen rendering for better screenshots. Offscreen only works at 1x resolution.
const forcedScaleFactor = switches &&
switches['force-device-scale-factor'] &&
switches['force-device-scale-factor'].toString() !== '1';
if (!options.webPreferences.hasOwnProperty('offscreen') && !forcedScaleFactor) {
options.webPreferences.offscreen = true;
}
else if (forcedScaleFactor) {
parent.emit('log', 'WARNING: Setting `forced-device-scale-factor` may cause screenshots to be out-of-date with respect to the current page contents.');
}

/**
* Create a new Browser Window
Expand Down Expand Up @@ -414,14 +425,48 @@ app.on('ready', function() {
*/

parent.respondTo('screenshot', function(path, clip, done) {
// https://gist.github.com/twolfson/0d374d9d7f26eefe7d38
var args = [function handleCapture (img) {
done(null, img.toPng());
}];
if (clip) args.unshift(clip);
frameManager.requestFrame(function() {
const MAXIMUM_WAIT_TIME = 1000;
let fallbackTimer;

function completeOperation (image) {
win.webContents.removeListener('paint', paintListener);
clearTimeout(fallbackTimer);
done(null, image.toPng());
}

// Get the rendered window from Chromium's backing store. The backing store does not update very frequently and
// may be out of date, so only use this when better options are unavailable.
function captureFromBackingStore () {
const args = [function handleCapture (image) {
completeOperation(null, image);
}];
if (clip) args.unshift(clip);
win.capturePage.apply(win, args);
});
}

// Handle actual paint events! This is generally faster than using the backing store, but more importantly, we can
// guarantee it's up-to-date.
function paintListener (event, dirty, image) {
const imageSize = image.getSize();
// Since we called invalidate, the frame we want will necessarily dirty the whole image... wait for that frame.
if (dirty.x === 0 && dirty.y === 0 && dirty.width === imageSize.width && dirty.height === imageSize.height) {
const clipped = clip ? image.crop(clip) : image;
completeOperation(clipped);
}
}

// Users *may* have disabled offscreen rendering, so we need to do the best we can based on current conditions/
// Note this does *not* mean the window isn't shown. It's an entirely separate mechanism.
if (win.webContents.isOffscreen()) {
fallbackTimer = setTimeout(captureFromBackingStore, MAXIMUM_WAIT_TIME);
win.webContents.on('paint', paintListener);
win.webContents.setFrameRate(60);
win.webContents.invalidate();
}
else {
parent.emit('log', 'screenshot: offscreen rendering is disabled; this screenshot may not match the current contents of the page.');
captureFromBackingStore();
}
});

/**
Expand Down

0 comments on commit 53dee8a

Please sign in to comment.