From c5a2810ae3408fcd3657e88c831784ad37776f61 Mon Sep 17 00:00:00 2001 From: mohanram-dev <328614055+mohanram-dev@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:34:52 +0530 Subject: [PATCH] fix(screenshot): error when webp dimensions exceed 16383px limit --- .../src/server/chromium/crPage.ts | 14 +++++--- .../src/server/screenshotter.ts | 2 ++ .../src/server/webkit/webview/wvPage.ts | 12 ++++--- .../src/server/webkit/wkPage.ts | 12 ++++--- .../src/tools/backend/screenshot.ts | 2 ++ tests/mcp/screenshot.spec.ts | 23 ++++++++++++ tests/page/page-screenshot.spec.ts | 36 +++++++++++++++++++ 7 files changed, 87 insertions(+), 14 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 17d2933f545c7..936af9d8c2b36 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -287,11 +287,15 @@ export class CRPage implements PageDelegate { // When taking screenshots with documentRect (based on the page content, not viewport), // ignore current page scale. const clip = { ...documentRect, scale: viewportRect ? visualViewport.scale : 1 }; - if (scale === 'css') { - // deviceScaleFactor override does not affect layout metrics, so if it is set, - // we use its value rather than computed one. - const deviceScaleFactor = this._mainFrameSession._metricsOverride?.deviceScaleFactor || contentSize.width / cssContentSize.width || 1; + const deviceScaleFactor = this._mainFrameSession._metricsOverride?.deviceScaleFactor || contentSize.width / cssContentSize.width || 1; + if (scale === 'css') clip.scale /= deviceScaleFactor; + if (format === 'webp') { + const renderScale = scale === 'css' ? clip.scale : clip.scale * deviceScaleFactor; + const width = Math.ceil(clip.width * renderScale); + const height = Math.ceil(clip.height * renderScale); + if (width > 16383 || height > 16383) + throw new Error('Cannot take screenshot larger than 16383 pixels on any dimension when using webp format'); } const result = await progress.race(this._mainFrameSession._client.send('Page.captureScreenshot', { format, quality, clip, captureBeyondViewport: !fitsViewport })); if (!fitsViewport && this._browserContext._options.hasTouch) { @@ -299,6 +303,8 @@ export class CRPage implements PageDelegate { // See https://issues.chromium.org/issues/558509412 and https://github.com/microsoft/playwright/issues/42607. await progress.race(this._mainFrameSession._client.send('Emulation.setTouchEmulationEnabled', { enabled: true })); } + if (!result.data) + throw new Error('Failed to take screenshot, received empty image data from browser'); return Buffer.from(result.data, 'base64'); } diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index 33db059bbb111..43cb7cdeb57ca 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -301,6 +301,8 @@ export class Screenshotter { try { const quality = format === 'jpeg' ? options.quality ?? 80 : format === 'webp' ? options.quality ?? 100 : undefined; const buffer = await this._page.delegate.takeScreenshot(progress, format, documentRect, viewportRect, quality, fitsViewport, options.scale || 'device'); + if (!buffer.byteLength) + throw new Error('Failed to take screenshot, received empty image data from browser'); await progress.race(cleanupHighlight()); if (shouldSetDefaultBackground) await progress.race(this._page.delegate.setBackgroundColor()); diff --git a/packages/playwright-core/src/server/webkit/webview/wvPage.ts b/packages/playwright-core/src/server/webkit/webview/wvPage.ts index 58ad96ad7ed9a..0030170c10617 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvPage.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvPage.ts @@ -765,13 +765,15 @@ export class WVPage implements PageDelegate { throw new Error('Method not implemented'); } - private validateScreenshotDimension(side: number, omitDeviceScaleFactor: boolean) { + private validateScreenshotDimension(side: number, omitDeviceScaleFactor: boolean, format: string) { + if (!omitDeviceScaleFactor && this._page.browserContext._options.deviceScaleFactor) + side = Math.ceil(side * this._page.browserContext._options.deviceScaleFactor); + if (format === 'webp' && side > 16383) + throw new Error('Cannot take screenshot larger than 16383 pixels on any dimension when using webp format'); // Cairo based implementations (Linux and Windows) have hard limit of 32767 // (see https://github.com/microsoft/playwright/issues/16727). if (process.platform === 'darwin') return; - if (!omitDeviceScaleFactor && this._page.browserContext._options.deviceScaleFactor) - side = Math.ceil(side * this._page.browserContext._options.deviceScaleFactor); if (side > 32767) throw new Error('Cannot take screenshot larger than 32767 pixels on any dimension'); } @@ -781,8 +783,8 @@ export class WVPage implements PageDelegate { const omitDeviceScaleFactor = scale === 'css'; if (omitDeviceScaleFactor) throw new Error('css screenshots are not implemented'); - this.validateScreenshotDimension(rect.width, omitDeviceScaleFactor); - this.validateScreenshotDimension(rect.height, omitDeviceScaleFactor); + this.validateScreenshotDimension(rect.width, omitDeviceScaleFactor, format); + this.validateScreenshotDimension(rect.height, omitDeviceScaleFactor, format); const result = await progress.race(this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport' })); const prefix = 'data:image/png;base64,'; let buffer: Buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64'); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 66373d187e02a..e454ab3703fb1 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -862,13 +862,15 @@ export class WKPage implements PageDelegate { return 0; } - private validateScreenshotDimension(side: number, omitDeviceScaleFactor: boolean) { + private validateScreenshotDimension(side: number, omitDeviceScaleFactor: boolean, format: string) { + if (!omitDeviceScaleFactor && this._page.browserContext._options.deviceScaleFactor) + side = Math.ceil(side * this._page.browserContext._options.deviceScaleFactor); + if (format === 'webp' && side > 16383) + throw new Error('Cannot take screenshot larger than 16383 pixels on any dimension when using webp format'); // Cairo based implementations (Linux and Windows) have hard limit of 32767 // (see https://github.com/microsoft/playwright/issues/16727). if (process.platform === 'darwin') return; - if (!omitDeviceScaleFactor && this._page.browserContext._options.deviceScaleFactor) - side = Math.ceil(side * this._page.browserContext._options.deviceScaleFactor); if (side > 32767) throw new Error('Cannot take screenshot larger than 32767 pixels on any dimension'); } @@ -876,8 +878,8 @@ 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 { const rect = (documentRect || viewportRect)!; const omitDeviceScaleFactor = scale === 'css'; - this.validateScreenshotDimension(rect.width, omitDeviceScaleFactor); - this.validateScreenshotDimension(rect.height, omitDeviceScaleFactor); + this.validateScreenshotDimension(rect.width, omitDeviceScaleFactor, format); + this.validateScreenshotDimension(rect.height, omitDeviceScaleFactor, format); // WebKit on macOS has no built-in WebP encoder, so capture a PNG and re-encode it. const recodePngToWebp = format === 'webp' && process.platform === 'darwin'; const result = await progress.race(this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport', omitDeviceScaleFactor, format: (recodePngToWebp ? 'png' : format) as 'png' | 'jpeg' | 'webp', quality: recodePngToWebp ? undefined : quality })); diff --git a/packages/playwright-core/src/tools/backend/screenshot.ts b/packages/playwright-core/src/tools/backend/screenshot.ts index a51091e8e07c3..4dfa2a781faa7 100644 --- a/packages/playwright-core/src/tools/backend/screenshot.ts +++ b/packages/playwright-core/src/tools/backend/screenshot.ts @@ -71,6 +71,8 @@ const screenshot = defineTabTool({ const screenshotTargetLabel = params.target ? params.element || 'element' : (params.fullPage ? 'full page' : 'viewport'); const target = params.target ? await tab.targetLocator({ element: params.element, target: params.target }) : null; const data = target ? await target.locator.screenshot(options) : await tab.page.screenshot(options); + if (!data.length) + throw new Error('Failed to take screenshot, received empty image data'); const resolvedFile = await response.resolveClientOutputFile({ prefix: target ? 'element' : 'page', ext: fileType, suggestedFilename: params.filename }, `Screenshot of ${screenshotTargetLabel}`); diff --git a/tests/mcp/screenshot.spec.ts b/tests/mcp/screenshot.spec.ts index ee7692432d8db..9ced5a50f2b57 100644 --- a/tests/mcp/screenshot.spec.ts +++ b/tests/mcp/screenshot.spec.ts @@ -497,3 +497,26 @@ test('browser_take_screenshot (viewport without snapshot)', async ({ startClient ], }); }); + +test('browser_take_screenshot (webp dimension limit)', async ({ startClient, server }, testInfo) => { + const outputDir = testInfo.outputPath('output'); + const { client } = await startClient({ + config: { outputDir }, + }); + server.setRoute('/tall.html', (req, res) => { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end('
'); + }); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX + '/tall.html' }, + }); + + const response = await client.callTool({ + name: 'browser_take_screenshot', + arguments: { type: 'webp', fullPage: true }, + }); + expect(response.content?.[0]?.text).toContain('Cannot take screenshot larger than 16383 pixels on any dimension when using webp format'); + const imageFiles = fs.existsSync(outputDir) ? fs.readdirSync(outputDir).filter(f => f.endsWith('.webp')) : []; + expect(imageFiles).toHaveLength(0); +}); diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index 6daca69121e88..4a71daa6aa963 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -984,6 +984,42 @@ it('should throw if screenshot size is too large', async ({ page, browserName, i } }); +it('should throw if webp screenshot size exceeds dimension limit', async ({ page, browserName }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42717' }); + it.skip(browserName === 'firefox', 'Firefox does not implement webp dimension limit'); + { + await page.setContent(`
`); + const result = await page.screenshot({ type: 'webp', fullPage: true }); + expect(result).toBeTruthy(); + } + { + await page.setContent(`
`); + const exception = await page.screenshot({ type: 'webp', fullPage: true }).catch(e => e); + expect(exception.message).toContain('Cannot take screenshot larger than 16383 pixels on any dimension when using webp format'); + } +}); + +it('should throw if webp screenshot size exceeds dimension limit with device scale factor', async ({ browser, browserName }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42717' }); + it.skip(browserName === 'firefox', 'Firefox does not implement webp dimension limit'); + const context = await browser.newContext({ viewport: { width: 500, height: 500 }, deviceScaleFactor: 2 }); + const page = await context.newPage(); + { + await page.setContent(`
`); + const result = await page.screenshot({ type: 'webp', fullPage: true }); + expect(result).toBeTruthy(); + } + { + await page.setContent(`
`); + const exception = await page.screenshot({ type: 'webp', fullPage: true }).catch(e => e); + expect(exception.message).toContain('Cannot take screenshot larger than 16383 pixels on any dimension when using webp format'); + + const image = await page.screenshot({ type: 'webp', fullPage: true, scale: 'css' }); + expect(image).toBeTruthy(); + } + await context.close(); +}); + it('page screenshot should capture css transform', async function({ page, browserName, isElectron, isAndroid }) { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/26447' }); it.fixme(browserName === 'webkit');