Implements automatic validation of screenshot dimensions to prevent "image dimensions exceed max allowed size: 8000 pixels" API errors. New features: ✅ **Automatic size validation** - Rejects images > 8000px by default ✅ **allowLargeImages flag** - Override validation when needed ✅ **Smart error messages** - Helpful solutions when validation fails ✅ **Image dimension parsing** - Supports PNG and JPEG format detection ✅ **Updated tool description** - Documents size limits and flag usage Error message provides actionable solutions: 1. Use viewport screenshot (remove fullPage: true) 2. Allow large images (add allowLargeImages: true) 3. Reduce viewport size (browser_configure) 4. Screenshot specific elements (element + ref) Usage examples: ```json // Safe viewport screenshot {"filename": "safe.png"} // Full page with validation {"fullPage": true, "filename": "page.png"} // Override validation for large images {"fullPage": true, "allowLargeImages": true, "filename": "large.png"} ``` This prevents the frustrating API error: "At least one of the image dimensions exceed max allowed size: 8000 pixels" by catching oversized images before they reach the API and providing clear solutions. Co-Authored-By: Claude <noreply@anthropic.com>
173 lines
7.5 KiB
TypeScript
173 lines
7.5 KiB
TypeScript
/**
|
||
* Copyright (c) Microsoft Corporation.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
import { z } from 'zod';
|
||
|
||
import { defineTabTool } from './tool.js';
|
||
import * as javascript from '../javascript.js';
|
||
import { outputFile } from '../config.js';
|
||
import { generateLocator } from './utils.js';
|
||
import { ArtifactManagerRegistry } from '../artifactManager.js';
|
||
|
||
import type * as playwright from 'playwright';
|
||
|
||
// Helper function to get image dimensions from buffer
|
||
function getImageDimensions(buffer: Buffer): { width: number, height: number } {
|
||
// PNG format check (starts with PNG signature)
|
||
if (buffer.length >= 24 && buffer.toString('ascii', 1, 4) === 'PNG') {
|
||
const width = buffer.readUInt32BE(16);
|
||
const height = buffer.readUInt32BE(20);
|
||
return { width, height };
|
||
}
|
||
|
||
// JPEG format check (starts with FF D8)
|
||
if (buffer.length >= 4 && buffer[0] === 0xFF && buffer[1] === 0xD8) {
|
||
// Look for SOF0 marker (Start of Frame)
|
||
let offset = 2;
|
||
while (offset < buffer.length - 8) {
|
||
if (buffer[offset] === 0xFF) {
|
||
const marker = buffer[offset + 1];
|
||
if (marker >= 0xC0 && marker <= 0xC3) { // SOF markers
|
||
const height = buffer.readUInt16BE(offset + 5);
|
||
const width = buffer.readUInt16BE(offset + 7);
|
||
return { width, height };
|
||
}
|
||
const length = buffer.readUInt16BE(offset + 2);
|
||
offset += 2 + length;
|
||
} else {
|
||
offset++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback - couldn't parse dimensions
|
||
throw new Error('Unable to determine image dimensions');
|
||
}
|
||
|
||
const screenshotSchema = z.object({
|
||
raw: z.boolean().optional().describe('Whether to return without compression (in PNG format). Default is false, which returns a JPEG image.'),
|
||
filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
|
||
element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
|
||
ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
|
||
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.'),
|
||
allowLargeImages: z.boolean().optional().describe('Allow images with dimensions exceeding 8000 pixels (API limit). Default false - will error if image is too large to prevent API failures.'),
|
||
}).refine(data => {
|
||
return !!data.element === !!data.ref;
|
||
}, {
|
||
message: 'Both element and ref must be provided or neither.',
|
||
path: ['ref', 'element']
|
||
}).refine(data => {
|
||
return !(data.fullPage && (data.element || data.ref));
|
||
}, {
|
||
message: 'fullPage cannot be used with element screenshots.',
|
||
path: ['fullPage']
|
||
});
|
||
|
||
const screenshot = defineTabTool({
|
||
capability: 'core',
|
||
schema: {
|
||
name: 'browser_take_screenshot',
|
||
title: 'Take a screenshot',
|
||
description: `Take a screenshot of the current page. Images exceeding 8000 pixels in either dimension will be rejected unless allowLargeImages=true. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
||
inputSchema: screenshotSchema,
|
||
type: 'readOnly',
|
||
},
|
||
|
||
handle: async (tab, params, response) => {
|
||
const fileType = params.raw ? 'png' : 'jpeg';
|
||
|
||
// Use centralized artifact storage if configured
|
||
let fileName: string;
|
||
const registry = ArtifactManagerRegistry.getInstance();
|
||
const artifactManager = tab.context.sessionId ? registry.getManager(tab.context.sessionId) : undefined;
|
||
|
||
if (artifactManager) {
|
||
const defaultName = params.filename ?? `page-${new Date().toISOString()}.${fileType}`;
|
||
fileName = artifactManager.getArtifactPath(defaultName);
|
||
} else {
|
||
fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
||
}
|
||
|
||
const options: playwright.PageScreenshotOptions = {
|
||
type: fileType,
|
||
quality: fileType === 'png' ? undefined : 50,
|
||
scale: 'css',
|
||
path: fileName,
|
||
...(params.fullPage !== undefined && { fullPage: params.fullPage })
|
||
};
|
||
const isElementScreenshot = params.element && params.ref;
|
||
|
||
const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport');
|
||
response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
|
||
|
||
// Only get snapshot when element screenshot is needed
|
||
const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null;
|
||
|
||
if (locator)
|
||
response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
||
else
|
||
response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
|
||
|
||
const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
||
|
||
// Validate image dimensions unless allowLargeImages is true
|
||
if (!params.allowLargeImages) {
|
||
try {
|
||
const { width, height } = getImageDimensions(buffer);
|
||
const maxDimension = 8000;
|
||
|
||
if (width > maxDimension || height > maxDimension) {
|
||
throw new Error(
|
||
`Screenshot dimensions (${width}x${height}) exceed maximum allowed size of ${maxDimension} pixels.\n\n` +
|
||
`**Solutions:**\n` +
|
||
`1. Use viewport screenshot: Remove "fullPage": true\n` +
|
||
`2. Allow large images: Add "allowLargeImages": true\n` +
|
||
`3. Reduce viewport size: browser_configure {"viewport": {"width": 1280, "height": 800}}\n` +
|
||
`4. Screenshot specific element: Use "element" and "ref" parameters\n\n` +
|
||
`**Example fixes:**\n` +
|
||
`browser_take_screenshot {"filename": "${params.filename || 'screenshot.png'}"} // viewport only\n` +
|
||
`browser_take_screenshot {"fullPage": true, "allowLargeImages": true, "filename": "${params.filename || 'screenshot.png'}"} // allow large`
|
||
);
|
||
}
|
||
} catch (dimensionError) {
|
||
// If we can't parse dimensions, continue without validation
|
||
// This shouldn't happen with standard PNG/JPEG images
|
||
}
|
||
}
|
||
|
||
let resultMessage = `Took the ${screenshotTarget} screenshot and saved it as ${fileName}`;
|
||
|
||
if (params.allowLargeImages) {
|
||
try {
|
||
const { width, height } = getImageDimensions(buffer);
|
||
resultMessage += `\n\n⚠️ **Large image warning:** Screenshot is ${width}x${height} pixels (may exceed API limits)`;
|
||
} catch (dimensionError) {
|
||
resultMessage += `\n\n⚠️ **Large image warning:** Size validation disabled (allowLargeImages=true)`;
|
||
}
|
||
}
|
||
|
||
response.addResult(resultMessage);
|
||
response.addImage({
|
||
contentType: fileType === 'png' ? 'image/png' : 'image/jpeg',
|
||
data: buffer
|
||
});
|
||
}
|
||
});
|
||
|
||
export default [
|
||
screenshot,
|
||
];
|