Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,7 @@ relative path, then it is resolved relative to the current working directory. If
saved to the disk.

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

Specify screenshot type, defaults to `png`.

Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12313,7 +12313,7 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}): Promise<Buffer>;

/**
Expand Down Expand Up @@ -24124,7 +24124,7 @@ export interface LocatorScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

interface ElementHandleWaitForSelectorOptions {
Expand Down Expand Up @@ -24797,7 +24797,7 @@ export interface PageScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

type Devices = {
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/client/elementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,15 @@ export async function convertInputFiles(platform: Platform, files: string | File
return { payloads };
}

export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' | 'webp' }): 'png' | 'jpeg' | 'webp' | undefined {
if (options.path) {
const mimeType = getMimeTypeForPath(options.path);
if (mimeType === 'image/png')
return 'png';
else if (mimeType === 'image/jpeg')
return 'jpeg';
else if (mimeType === 'image/webp')
return 'webp';
throw new Error(`path: unsupported mime type "${mimeType}"`);
}
return options.type;
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1872,7 +1872,7 @@ scheme.ElementHandleQuerySelectorAllResult = tObject({
});
scheme.ElementHandleScreenshotParams = tObject({
timeout: tFloat,
type: tOptional(tEnum(['png', 'jpeg'])),
type: tOptional(tEnum(['png', 'jpeg', 'webp'])),
quality: tOptional(tInt),
omitBackground: tOptional(tBoolean),
caret: tOptional(tEnum(['hide', 'initial'])),
Expand Down Expand Up @@ -2430,7 +2430,7 @@ scheme.PageExpectScreenshotResult = tObject({
});
scheme.PageScreenshotParams = tObject({
timeout: tFloat,
type: tOptional(tEnum(['png', 'jpeg'])),
type: tOptional(tEnum(['png', 'jpeg', 'webp'])),
quality: tOptional(tInt),
fullPage: tOptional(tBoolean),
clip: tOptional(tType('Rect')),
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/bidi/bidiPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,8 @@ export class BidiPage implements PageDelegate {
}

async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
if (format === 'webp')
throw new Error('webp screenshots are not supported via WebDriver BiDi');
const rect = (documentRect || viewportRect)!;
const { data } = await progress.race(this._session.send('browsingContext.captureScreenshot', {
context: this._session.sessionId,
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export class CRPage implements PageDelegate {
await this._mainFrameSession._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
}

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> {
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> {
const { visualViewport, contentSize, cssContentSize } = await progress.race(this._mainFrameSession._client.send('Page.getLayoutMetrics'));
if (!documentRect) {
documentRect = {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/firefox/ffPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ export class FFPage implements PageDelegate {
throw new Error('Not implemented');
}

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> {
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> {
if (!documentRect) {
const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY }));
documentRect = {
Expand All @@ -466,7 +466,7 @@ export class FFPage implements PageDelegate {
};
}
const { data } = await progress.race(this._session.send('Page.screenshot', {
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'),
mimeType: ('image/' + format) as ('image/png' | 'image/jpeg' | 'image/webp'),
clip: documentRect,
quality,
omitDeviceScaleFactor: scale === 'css',
Expand Down
14 changes: 7 additions & 7 deletions packages/playwright-core/src/server/screenshotter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ declare global {
}

export type ScreenshotOptions = {
type?: 'png' | 'jpeg';
type?: 'png' | 'jpeg' | 'webp';
quality?: number;
omitBackground?: boolean;
animations?: 'disabled' | 'allow';
Expand Down Expand Up @@ -297,7 +297,7 @@ export class Screenshotter {
}
}

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

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

try {
const quality = format === 'jpeg' ? options.quality ?? 80 : undefined;
const quality = format === 'jpeg' ? options.quality ?? 80 : format === 'webp' ? options.quality : undefined;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No default quality for webp?

const buffer = await this._page.delegate.takeScreenshot(progress, format, documentRect, viewportRect, quality, fitsViewport, options.scale || 'device');
await progress.race(cleanupHighlight());
if (shouldSetDefaultBackground)
Expand Down Expand Up @@ -353,20 +353,20 @@ function trimClipToSize(clip: types.Rect, size: types.Size): types.Rect {
return result;
}

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

if (!format)
format = 'png';

if (options.quality !== undefined) {
assert(format === 'jpeg', 'options.quality is unsupported for the ' + format + ' screenshots');
assert(format !== 'png', 'options.quality is unsupported for the ' + format + ' screenshots');
assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality));
assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer');
assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality);
Expand Down
13 changes: 5 additions & 8 deletions packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
* limitations under the License.
*/

import { PNG } from 'pngjs';
import jpegjs from 'jpeg-js';
import { headersArrayToObject, headersObjectToArray } from '@isomorphic/headers';
import { splitErrorMessage } from '@isomorphic/stackTrace';
import { eventsHelper } from '@utils/eventsHelper';
Expand Down Expand Up @@ -870,16 +868,15 @@ export class WKPage implements PageDelegate {
}

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

async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/tools/backend/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class Response {
readonly toolName: string;
readonly toolArgs: Record<string, any>;
private _clientWorkspace: string;
private _imageResults: { data: Buffer, imageType: 'png' | 'jpeg' }[] = [];
private _imageResults: { data: Buffer, imageType: 'png' | 'jpeg' | 'webp' }[] = [];
private _raw: boolean;
private _json: boolean;
private _writtenFiles = new Set<string>();
Expand Down Expand Up @@ -127,7 +127,7 @@ export class Response {
this.addTextResult(`- [${title}](${relativeName})`);
}

async registerImageResult(data: Buffer, imageType: 'png' | 'jpeg') {
async registerImageResult(data: Buffer, imageType: 'png' | 'jpeg' | 'webp') {
this._imageResults.push({ data, imageType });
}

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

Expand Down
30 changes: 25 additions & 5 deletions packages/playwright-core/src/tools/backend/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import path from 'path';

import jpegjs from 'jpeg-js';
import { PNG } from 'pngjs';
import * as z from 'zod';
Expand All @@ -25,12 +27,26 @@ import { optionalElementSchema } from './snapshot';

import type * as playwright from '../../..';

type ImageFormat = 'png' | 'jpeg' | 'webp';

const screenshotSchema = optionalElementSchema.extend({
type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory.'),
type: z.enum(['png', 'jpeg', 'webp']).optional().describe('Image format for the screenshot. If unset, inferred from the filename extension, otherwise png.'),
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg|webp}` if not specified. Prefer relative file names to stay within the output directory.'),
fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.'),
});

function inferTypeFromFilename(filename: string | undefined): ImageFormat | undefined {
if (!filename)
return undefined;
switch (path.extname(filename).toLowerCase()) {
case '.png': return 'png';
case '.jpg':
case '.jpeg': return 'jpeg';
case '.webp': return 'webp';
}
return undefined;
}

const screenshot = defineTabTool({
capability: 'core',
schema: {
Expand All @@ -45,10 +61,10 @@ const screenshot = defineTabTool({
if (params.fullPage && params.target)
throw new Error('fullPage cannot be used with element screenshots.');

const fileType = params.type || 'png';
const fileType: ImageFormat = params.type ?? inferTypeFromFilename(params.filename) ?? 'png';
const options: playwright.PageScreenshotOptions = {
type: fileType,
quality: fileType === 'png' ? undefined : 90,
quality: fileType === 'jpeg' ? 90 : undefined,
scale: 'css',
...tab.actionTimeoutOptions,
...(params.fullPage !== undefined && { fullPage: params.fullPage })
Expand All @@ -72,10 +88,14 @@ const screenshot = defineTabTool({
}
});

export function scaleImageToFitMessage(buffer: Buffer, imageType: 'png' | 'jpeg'): Buffer {
export function scaleImageToFitMessage(buffer: Buffer, imageType: 'png' | 'jpeg' | 'webp'): Buffer {
// https://docs.claude.com/en/docs/build-with-claude/vision#evaluate-image-size
// Not more than 1.15 megapixel, linear size not more than 1568.

// No Node-side webp decoder is bundled, so we can't scale webp screenshots.
if (imageType === 'webp')
return buffer;

const image = imageType === 'png' ? PNG.sync.read(buffer) : jpegjs.decode(buffer, { maxMemoryUsageInMB: 512 });
const pixels = image.width * image.height;

Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/tools/cli-daemon/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,11 +789,12 @@ const screenshot = declareCommand({
target: z.string().optional().describe(elementTargetDescription),
}),
options: z.object({
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg|webp}` if not specified.'),
type: z.enum(['png', 'jpeg', 'webp']).optional().describe('Image format. If unset, inferred from the filename extension, otherwise png.'),
['full-page']: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport.'),
}),
toolName: 'browser_take_screenshot',
toolParams: ({ target, filename, ['full-page']: fullPage }) => ({ filename, target, fullPage }),
toolParams: ({ target, filename, type, ['full-page']: fullPage }) => ({ filename, target, type, fullPage }),
});

const pdfSave = declareCommand({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ function generateCommandHelp(command: AnyCommandSchema) {
const optionsShape = (command.options as zodType.ZodObject<any>).shape;
for (const [name, schema] of Object.entries(optionsShape)) {
const zodSchema = schema as zodType.ZodTypeAny;
const description = (zodSchema.description ?? '').toLowerCase();
let description = (zodSchema.description ?? '').toLowerCase();
const unwrapped = unwrapZodType(zodSchema);
if (unwrapped instanceof z.ZodEnum)
description = `${description} (one of: ${unwrapped.options.join(', ')})`.trim();
lines.push(formatWithGap(` --${name}`, description));
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12313,7 +12313,7 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}): Promise<Buffer>;

/**
Expand Down Expand Up @@ -24124,7 +24124,7 @@ export interface LocatorScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

interface ElementHandleWaitForSelectorOptions {
Expand Down Expand Up @@ -24797,7 +24797,7 @@ export interface PageScreenshotOptions {
/**
* Specify screenshot type, defaults to `png`.
*/
type?: "png"|"jpeg";
type?: "png"|"jpeg"|"webp";
}

type Devices = {
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/spec/handles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ ElementHandle:
literals:
- png
- jpeg
- webp
quality: int?
$mixin: CommonScreenshotOptions
returns:
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/spec/page.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ Page:
literals:
- png
- jpeg
- webp
quality: int?
fullPage: boolean?
clip: Rect?
Expand Down
8 changes: 4 additions & 4 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3377,7 +3377,7 @@ export type ElementHandleQuerySelectorAllResult = {
};
export type ElementHandleScreenshotParams = {
timeout: number,
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
omitBackground?: boolean,
caret?: 'hide' | 'initial',
Expand All @@ -3391,7 +3391,7 @@ export type ElementHandleScreenshotParams = {
style?: string,
};
export type ElementHandleScreenshotOptions = {
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
omitBackground?: boolean,
caret?: 'hide' | 'initial',
Expand Down Expand Up @@ -4334,7 +4334,7 @@ export type PageExpectScreenshotResult = {
};
export type PageScreenshotParams = {
timeout: number,
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
fullPage?: boolean,
clip?: Rect,
Expand All @@ -4350,7 +4350,7 @@ export type PageScreenshotParams = {
style?: string,
};
export type PageScreenshotOptions = {
type?: 'png' | 'jpeg',
type?: 'png' | 'jpeg' | 'webp',
quality?: number,
fullPage?: boolean,
clip?: Rect,
Expand Down
Loading
Loading