Skip to content

Commit 1b863c2

Browse files
authored
fix(screenshots): simplify implementation, allow fullPage + clip, add tests (#1194)
1 parent 2ec9e6d commit 1b863c2

15 files changed

+212
-128
lines changed

docs/api.md

+8-9
Original file line numberDiff line numberDiff line change
@@ -1298,18 +1298,18 @@ await browser.close();
12981298
#### page.screenshot([options])
12991299
- `options` <[Object]> Options object which might have the following properties:
13001300
- `path` <[string]> The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk.
1301-
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to 'png'.
1301+
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to `png`.
13021302
- `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images.
1303-
- `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page. Defaults to `false`.
1304-
- `clip` <[Object]> An object which specifies clipping region of the page. Should have the following fields:
1303+
- `fullPage` <[boolean]> When true, takes a screenshot of the full scrollable page, instead of the currently visibvle viewport. Defaults to `false`.
1304+
- `clip` <[Object]> An object which specifies clipping of the resulting image. Should have the following fields:
13051305
- `x` <[number]> x-coordinate of top-left corner of clip area
13061306
- `y` <[number]> y-coordinate of top-left corner of clip area
13071307
- `width` <[number]> width of clipping area
13081308
- `height` <[number]> height of clipping area
1309-
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
1309+
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. Defaults to `false`.
13101310
- returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with the captured screenshot.
13111311

1312-
> **NOTE** Screenshots take at least 1/6 second on OS X. See https://crbug.com/741689 for discussion.
1312+
> **NOTE** Screenshots take at least 1/6 second on Chromium OS X and Chromium Windows. See https://crbug.com/741689 for discussion.
13131313
13141314
#### page.select(selector, value, options)
13151315
- `selector` <[string]> A selector to query frame for.
@@ -2483,13 +2483,12 @@ If `key` is a single character and no modifier keys besides `Shift` are being he
24832483
#### elementHandle.screenshot([options])
24842484
- `options` <[Object]> Screenshot options.
24852485
- `path` <[string]> The file path to save the image to. The screenshot type will be inferred from file extension. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). If no path is provided, the image won't be saved to the disk.
2486-
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to 'png'.
2486+
- `type` <"png"|"jpeg"> Specify screenshot type, defaults to `png`.
24872487
- `quality` <[number]> The quality of the image, between 0-100. Not applicable to `png` images.
2488-
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Defaults to `false`.
2488+
- `omitBackground` <[boolean]> Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. Defaults to `false`.
24892489
- returns: <[Promise]<|[Buffer]>> Promise which resolves to buffer with the captured screenshot.
24902490

2491-
This method scrolls element into view if needed, and then uses [page.screenshot](#pagescreenshotoptions) to take a screenshot of the element.
2492-
If the element is detached from DOM, the method throws an error.
2491+
This method scrolls element into view if needed before taking a screenshot. If the element is detached from DOM, the method throws an error.
24932492

24942493
#### elementHandle.scrollIntoViewIfNeeded()
24952494
- returns: <[Promise]> Resolves after the element has been scrolled into view.

src/chromium/crPage.ts

+16-13
Original file line numberDiff line numberDiff line change
@@ -417,16 +417,6 @@ export class CRPage implements PageDelegate {
417417
await this._browser._closePage(this._page);
418418
}
419419

420-
async getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
421-
const rect = await handle.boundingBox();
422-
if (!rect)
423-
return rect;
424-
const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
425-
rect.x += pageX;
426-
rect.y += pageY;
427-
return rect;
428-
}
429-
430420
canScreenshotOutsideViewport(): boolean {
431421
return false;
432422
}
@@ -435,10 +425,23 @@ export class CRPage implements PageDelegate {
435425
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
436426
}
437427

438-
async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions, viewportSize: types.Size): Promise<platform.BufferType> {
428+
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType> {
429+
const { visualViewport } = await this._client.send('Page.getLayoutMetrics');
430+
if (!documentRect) {
431+
documentRect = {
432+
x: visualViewport.pageX + viewportRect!.x,
433+
y: visualViewport.pageY + viewportRect!.y,
434+
...helper.enclosingIntSize({
435+
width: viewportRect!.width / visualViewport.scale,
436+
height: viewportRect!.height / visualViewport.scale,
437+
})
438+
};
439+
}
439440
await this._client.send('Page.bringToFront', {});
440-
const clip = options.clip ? { ...options.clip, scale: 1 } : undefined;
441-
const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip });
441+
// When taking screenshots with documentRect (based on the page content, not viewport),
442+
// ignore current page scale.
443+
const clip = { ...documentRect, scale: viewportRect ? visualViewport.scale : 1 };
444+
const result = await this._client.send('Page.captureScreenshot', { format, quality, clip });
442445
return platform.Buffer.from(result.data, 'base64');
443446
}
444447

src/firefox/ffPage.ts

+15-13
Original file line numberDiff line numberDiff line change
@@ -317,15 +317,6 @@ export class FFPage implements PageDelegate {
317317
await this._session.send('Page.close', { runBeforeUnload });
318318
}
319319

320-
async getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null> {
321-
const frameId = handle._context.frame._id;
322-
const response = await this._session.send('Page.getBoundingBox', {
323-
frameId,
324-
objectId: handle._remoteObject.objectId,
325-
});
326-
return response.boundingBox;
327-
}
328-
329320
canScreenshotOutsideViewport(): boolean {
330321
return true;
331322
}
@@ -335,11 +326,22 @@ export class FFPage implements PageDelegate {
335326
throw new Error('Not implemented');
336327
}
337328

338-
async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions, viewportSize: types.Size): Promise<platform.BufferType> {
329+
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType> {
330+
if (!documentRect) {
331+
const context = await this._page.mainFrame()._utilityContext();
332+
const scrollOffset = await context.evaluate(() => ({ x: window.scrollX, y: window.scrollY }));
333+
documentRect = {
334+
x: viewportRect!.x + scrollOffset.x,
335+
y: viewportRect!.y + scrollOffset.y,
336+
width: viewportRect!.width,
337+
height: viewportRect!.height,
338+
};
339+
}
340+
// TODO: remove fullPage option from Page.screenshot.
341+
// TODO: remove Page.getBoundingBox method.
339342
const { data } = await this._session.send('Page.screenshot', {
340343
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
341-
fullPage: options.fullPage,
342-
clip: options.clip,
344+
clip: documentRect,
343345
}).catch(e => {
344346
if (e instanceof Error && e.message.includes('document.documentElement is null'))
345347
e.message = kScreenshotDuringNavigationError;
@@ -349,7 +351,7 @@ export class FFPage implements PageDelegate {
349351
}
350352

351353
async resetViewport(): Promise<void> {
352-
await this._session.send('Page.setViewportSize', { viewportSize: null });
354+
assert(false, 'Should not be called');
353355
}
354356

355357
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {

src/helper.ts

+13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { TimeoutError } from './errors';
1919
import * as platform from './platform';
20+
import * as types from './types';
2021

2122
export const debugError = platform.debug(`pw:error`);
2223

@@ -266,6 +267,18 @@ class Helper {
266267
const rightHalf = maxLength - leftHalf - 1;
267268
return string.substr(0, leftHalf) + '\u2026' + string.substr(this.length - rightHalf, rightHalf);
268269
}
270+
271+
static enclosingIntRect(rect: types.Rect): types.Rect {
272+
const x = Math.floor(rect.x + 1e-3);
273+
const y = Math.floor(rect.y + 1e-3);
274+
const x2 = Math.ceil(rect.x + rect.width - 1e-3);
275+
const y2 = Math.ceil(rect.y + rect.height - 1e-3);
276+
return { x, y, width: x2 - x, height: y2 - y };
277+
}
278+
279+
static enclosingIntSize(size: types.Size): types.Size {
280+
return { width: Math.floor(size.width + 1e-3), height: Math.floor(size.height + 1e-3) };
281+
}
269282
}
270283

271284
export function assert(value: any, message?: string): asserts value {

src/page.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,10 @@ export interface PageDelegate {
5454
authenticate(credentials: types.Credentials | null): Promise<void>;
5555
setFileChooserIntercepted(enabled: boolean): Promise<void>;
5656

57-
getBoundingBoxForScreenshot(handle: dom.ElementHandle<Node>): Promise<types.Rect | null>;
5857
canScreenshotOutsideViewport(): boolean;
58+
resetViewport(): Promise<void>; // Only called if canScreenshotOutsideViewport() returns false.
5959
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
60-
takeScreenshot(format: string, options: types.ScreenshotOptions, viewportSize: types.Size): Promise<platform.BufferType>;
61-
resetViewport(oldSize: types.Size): Promise<void>;
60+
takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType>;
6261

6362
isElementHandle(remoteObject: any): boolean;
6463
adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>>;

src/platform.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ export function urlMatches(urlString: string, match: types.URLMatch | undefined)
234234
return match(url);
235235
}
236236

237-
export function pngToJpeg(buffer: Buffer): Buffer {
237+
export function pngToJpeg(buffer: Buffer, quality?: number): Buffer {
238238
assert(isNode, 'Converting from png to jpeg is only supported in Node.js');
239-
return jpeg.encode(png.PNG.sync.read(buffer)).data;
239+
return jpeg.encode(png.PNG.sync.read(buffer), quality).data;
240240
}
241241

242242
function nodeFetch(url: string): Promise<string> {

0 commit comments

Comments
 (0)