Skip to content

Commit e061865

Browse files
committed
feat(screenshot): add webp output format
Adds `webp` to the screenshot type union for `page.screenshot()`, `locator.screenshot()`, the MCP `browser_take_screenshot` tool, and the playwright-cli `screenshot` command. Encoded natively by each browser — CDP for Chromium, `Page.screenshot` for Firefox, and `Page.snapshotRect` (with format + quality) for WebKit; the WebKit path also drops the PNG→JPEG re-encode round-trip in Node. The MCP/CLI tools infer the format from the filename extension when `type` is unset. The CLI help generator now lists enum choices for any `z.enum` option. WebP screenshots are not supported in WebKit on macOS because CoreGraphics does not ship a webp encoder UTI; the WebKit delegate throws a clear error there. Fixes microsoft/playwright-mcp#1632
1 parent 0492b3b commit e061865

20 files changed

Lines changed: 115 additions & 45 deletions

File tree

docs/src/api/params.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1287,7 +1287,7 @@ relative path, then it is resolved relative to the current working directory. If
12871287
saved to the disk.
12881288

12891289
## screenshot-option-type
1290-
- `type` <[ScreenshotType]<"png"|"jpeg">>
1290+
- `type` <[ScreenshotType]<"png"|"jpeg"|"webp">>
12911291

12921292
Specify screenshot type, defaults to `png`.
12931293

packages/playwright-client/types/types.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12313,7 +12313,7 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
1231312313
/**
1231412314
* Specify screenshot type, defaults to `png`.
1231512315
*/
12316-
type?: "png"|"jpeg";
12316+
type?: "png"|"jpeg"|"webp";
1231712317
}): Promise<Buffer>;
1231812318

1231912319
/**
@@ -24124,7 +24124,7 @@ export interface LocatorScreenshotOptions {
2412424124
/**
2412524125
* Specify screenshot type, defaults to `png`.
2412624126
*/
24127-
type?: "png"|"jpeg";
24127+
type?: "png"|"jpeg"|"webp";
2412824128
}
2412924129

2413024130
interface ElementHandleWaitForSelectorOptions {
@@ -24797,7 +24797,7 @@ export interface PageScreenshotOptions {
2479724797
/**
2479824798
* Specify screenshot type, defaults to `png`.
2479924799
*/
24800-
type?: "png"|"jpeg";
24800+
type?: "png"|"jpeg"|"webp";
2480124801
}
2480224802

2480324803
type Devices = {

packages/playwright-core/src/client/elementHandle.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,15 @@ export async function convertInputFiles(platform: Platform, files: string | File
317317
return { payloads };
318318
}
319319

320-
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
320+
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' | 'webp' }): 'png' | 'jpeg' | 'webp' | undefined {
321321
if (options.path) {
322322
const mimeType = getMimeTypeForPath(options.path);
323323
if (mimeType === 'image/png')
324324
return 'png';
325325
else if (mimeType === 'image/jpeg')
326326
return 'jpeg';
327+
else if (mimeType === 'image/webp')
328+
return 'webp';
327329
throw new Error(`path: unsupported mime type "${mimeType}"`);
328330
}
329331
return options.type;

packages/playwright-core/src/protocol/validator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1872,7 +1872,7 @@ scheme.ElementHandleQuerySelectorAllResult = tObject({
18721872
});
18731873
scheme.ElementHandleScreenshotParams = tObject({
18741874
timeout: tFloat,
1875-
type: tOptional(tEnum(['png', 'jpeg'])),
1875+
type: tOptional(tEnum(['png', 'jpeg', 'webp'])),
18761876
quality: tOptional(tInt),
18771877
omitBackground: tOptional(tBoolean),
18781878
caret: tOptional(tEnum(['hide', 'initial'])),
@@ -2430,7 +2430,7 @@ scheme.PageExpectScreenshotResult = tObject({
24302430
});
24312431
scheme.PageScreenshotParams = tObject({
24322432
timeout: tFloat,
2433-
type: tOptional(tEnum(['png', 'jpeg'])),
2433+
type: tOptional(tEnum(['png', 'jpeg', 'webp'])),
24342434
quality: tOptional(tInt),
24352435
fullPage: tOptional(tBoolean),
24362436
clip: tOptional(tType('Rect')),

packages/playwright-core/src/server/bidi/bidiPage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ export class BidiPage implements PageDelegate {
479479
}
480480

481481
async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
482+
if (format === 'webp')
483+
throw new Error('webp screenshots are not supported via WebDriver BiDi');
482484
const rect = (documentRect || viewportRect)!;
483485
const { data } = await progress.race(this._session.send('browsingContext.captureScreenshot', {
484486
context: this._session.sessionId,

packages/playwright-core/src/server/chromium/crPage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export class CRPage implements PageDelegate {
247247
await this._mainFrameSession._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
248248
}
249249

250-
async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
250+
async takeScreenshot(progress: Progress, format: 'png' | 'jpeg' | 'webp', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
251251
const { visualViewport, contentSize, cssContentSize } = await progress.race(this._mainFrameSession._client.send('Page.getLayoutMetrics'));
252252
if (!documentRect) {
253253
documentRect = {

packages/playwright-core/src/server/firefox/ffPage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ export class FFPage implements PageDelegate {
455455
throw new Error('Not implemented');
456456
}
457457

458-
async takeScreenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
458+
async takeScreenshot(progress: Progress, format: 'png' | 'jpeg' | 'webp', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
459459
if (!documentRect) {
460460
const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY }));
461461
documentRect = {
@@ -466,7 +466,7 @@ export class FFPage implements PageDelegate {
466466
};
467467
}
468468
const { data } = await progress.race(this._session.send('Page.screenshot', {
469-
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
469+
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg' | 'image/webp'),
470470
clip: documentRect,
471471
quality,
472472
omitDeviceScaleFactor: scale === 'css',

packages/playwright-core/src/server/screenshotter.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ declare global {
3535
}
3636

3737
export type ScreenshotOptions = {
38-
type?: 'png' | 'jpeg';
38+
type?: 'png' | 'jpeg' | 'webp';
3939
quality?: number;
4040
omitBackground?: boolean;
4141
animations?: 'disabled' | 'allow';
@@ -297,7 +297,7 @@ export class Screenshotter {
297297
}
298298
}
299299

300-
private async _screenshot(progress: Progress, format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean, options: ScreenshotOptions): Promise<Buffer> {
300+
private async _screenshot(progress: Progress, format: 'png' | 'jpeg' | 'webp', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, fitsViewport: boolean, options: ScreenshotOptions): Promise<Buffer> {
301301
if ((options as any).__testHookBeforeScreenshot)
302302
await progress.race((options as any).__testHookBeforeScreenshot());
303303

@@ -307,7 +307,7 @@ export class Screenshotter {
307307
const cleanupHighlight = await this._maskElements(progress, options);
308308

309309
try {
310-
const quality = format === 'jpeg' ? options.quality ?? 80 : undefined;
310+
const quality = format === 'jpeg' ? options.quality ?? 80 : format === 'webp' ? options.quality : undefined;
311311
const buffer = await this._page.delegate.takeScreenshot(progress, format, documentRect, viewportRect, quality, fitsViewport, options.scale || 'device');
312312
await progress.race(cleanupHighlight());
313313
if (shouldSetDefaultBackground)
@@ -353,20 +353,20 @@ function trimClipToSize(clip: types.Rect, size: types.Size): types.Rect {
353353
return result;
354354
}
355355

356-
export function validateScreenshotOptions(options: ScreenshotOptions): 'png' | 'jpeg' {
357-
let format: 'png' | 'jpeg' | null = null;
356+
export function validateScreenshotOptions(options: ScreenshotOptions): 'png' | 'jpeg' | 'webp' {
357+
let format: 'png' | 'jpeg' | 'webp' | null = null;
358358
// options.type takes precedence over inferring the type from options.path
359359
// because it may be a 0-length file with no extension created beforehand (i.e. as a temp file).
360360
if (options.type) {
361-
assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
361+
assert(options.type === 'png' || options.type === 'jpeg' || options.type === 'webp', 'Unknown options.type value: ' + options.type);
362362
format = options.type;
363363
}
364364

365365
if (!format)
366366
format = 'png';
367367

368368
if (options.quality !== undefined) {
369-
assert(format === 'jpeg', 'options.quality is unsupported for the ' + format + ' screenshots');
369+
assert(format !== 'png', 'options.quality is unsupported for the ' + format + ' screenshots');
370370
assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality));
371371
assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
372372
assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality);

packages/playwright-core/src/server/webkit/wkPage.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { PNG } from 'pngjs';
19-
import jpegjs from 'jpeg-js';
2018
import { headersArrayToObject, headersObjectToArray } from '@isomorphic/headers';
2119
import { splitErrorMessage } from '@isomorphic/stackTrace';
2220
import { eventsHelper } from '@utils/eventsHelper';
@@ -870,16 +868,15 @@ export class WKPage implements PageDelegate {
870868
}
871869

872870
async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
871+
if (format === 'webp' && process.platform === 'darwin')
872+
throw new Error('webp screenshots are not supported in WebKit on macOS');
873873
const rect = (documentRect || viewportRect)!;
874874
const omitDeviceScaleFactor = scale === 'css';
875875
this.validateScreenshotDimension(rect.width, omitDeviceScaleFactor);
876876
this.validateScreenshotDimension(rect.height, omitDeviceScaleFactor);
877-
const result = await progress.race(this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor }));
878-
const prefix = 'data:image/png;base64,';
879-
let buffer: Buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
880-
if (format === 'jpeg')
881-
buffer = jpegjs.encode(PNG.sync.read(buffer), quality).data;
882-
return buffer;
877+
const result = await progress.race(this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor, format: format as 'png' | 'jpeg' | 'webp', quality }));
878+
// Strip 'data:image/png;base64,' prefix.
879+
return Buffer.from(result.dataURL.substring(result.dataURL.indexOf(',') + 1), 'base64');
883880
}
884881

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

packages/playwright-core/src/tools/backend/response.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class Response {
5858
readonly toolName: string;
5959
readonly toolArgs: Record<string, any>;
6060
private _clientWorkspace: string;
61-
private _imageResults: { data: Buffer, imageType: 'png' | 'jpeg' }[] = [];
61+
private _imageResults: { data: Buffer, imageType: 'png' | 'jpeg' | 'webp' }[] = [];
6262
private _raw: boolean;
6363
private _json: boolean;
6464
private _writtenFiles = new Set<string>();
@@ -127,7 +127,7 @@ export class Response {
127127
this.addTextResult(`- [${title}](${relativeName})`);
128128
}
129129

130-
async registerImageResult(data: Buffer, imageType: 'png' | 'jpeg') {
130+
async registerImageResult(data: Buffer, imageType: 'png' | 'jpeg' | 'webp') {
131131
this._imageResults.push({ data, imageType });
132132
}
133133

@@ -219,7 +219,7 @@ export class Response {
219219
if (this._context.config.imageResponses !== 'omit') {
220220
for (const imageResult of this._imageResults) {
221221
const scaledData = scaleImageToFitMessage(imageResult.data, imageResult.imageType);
222-
content.push({ type: 'image', data: scaledData.toString('base64'), mimeType: imageResult.imageType === 'png' ? 'image/png' : 'image/jpeg' });
222+
content.push({ type: 'image', data: scaledData.toString('base64'), mimeType: `image/${imageResult.imageType}` });
223223
}
224224
}
225225

0 commit comments

Comments
 (0)