Skip to content

Commit 9fa46ee

Browse files
marcandreufclaude
andcommitted
fix: detect actual image format in take_screenshot tool
Fixes #583 The take_screenshot tool was incorrectly declaring the MIME type based on the requested format parameter rather than detecting the actual format of the image data returned by Puppeteer. This caused a mismatch when Puppeteer returned a different format than requested, leading to Claude API validation errors like: "Image does not match the provided media type image/jpeg" Changes: - Add detectImageFormat() utility to identify actual image format from binary data by inspecting magic numbers (PNG, JPEG, WebP) - Update screenshot tool to detect and use the actual format instead of blindly trusting the request parameter - Add comprehensive unit tests for format detection Co-Authored-By: Claude <[email protected]>
1 parent 6f9182f commit 9fa46ee

File tree

3 files changed

+139
-2
lines changed

3 files changed

+139
-2
lines changed

src/tools/screenshot.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import {zod} from '../third_party/index.js';
88
import type {ElementHandle, Page} from '../third_party/index.js';
99

10+
import {detectImageFormat} from '../utils/imageFormat.js';
1011
import {ToolCategory} from './categories.js';
1112
import {defineTool} from './ToolDefinition.js';
1213

@@ -83,18 +84,22 @@ export const screenshot = defineTool({
8384
);
8485
}
8586

87+
// Detect the actual format of the screenshot data
88+
// Puppeteer may not always return the requested format
89+
const actualFormat = detectImageFormat(screenshot);
90+
8691
if (request.params.filePath) {
8792
const file = await context.saveFile(screenshot, request.params.filePath);
8893
response.appendResponseLine(`Saved screenshot to ${file.filename}.`);
8994
} else if (screenshot.length >= 2_000_000) {
9095
const {filename} = await context.saveTemporaryFile(
9196
screenshot,
92-
`image/${request.params.format}`,
97+
actualFormat,
9398
);
9499
response.appendResponseLine(`Saved screenshot to ${filename}.`);
95100
} else {
96101
response.attachImage({
97-
mimeType: `image/${request.params.format}`,
102+
mimeType: actualFormat,
98103
data: Buffer.from(screenshot).toString('base64'),
99104
});
100105
}

src/utils/imageFormat.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* Detects the actual image format from binary data by inspecting magic numbers.
9+
*
10+
* @param data - The image data as a Uint8Array or Buffer
11+
* @returns The detected MIME type ('image/png', 'image/jpeg', or 'image/webp')
12+
* @throws Error if the format cannot be detected
13+
*/
14+
export function detectImageFormat(
15+
data: Uint8Array | Buffer,
16+
): 'image/png' | 'image/jpeg' | 'image/webp' {
17+
if (data.length < 12) {
18+
throw new Error('Image data too small to detect format');
19+
}
20+
21+
// Check PNG: starts with 89 50 4E 47 (‰PNG)
22+
if (
23+
data[0] === 0x89 &&
24+
data[1] === 0x50 &&
25+
data[2] === 0x4e &&
26+
data[3] === 0x47
27+
) {
28+
return 'image/png';
29+
}
30+
31+
// Check JPEG: starts with FF D8 FF
32+
if (data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff) {
33+
return 'image/jpeg';
34+
}
35+
36+
// Check WebP: starts with "RIFF" and contains "WEBP" at offset 8
37+
if (
38+
data[0] === 0x52 && // R
39+
data[1] === 0x49 && // I
40+
data[2] === 0x46 && // F
41+
data[3] === 0x46 && // F
42+
data[8] === 0x57 && // W
43+
data[9] === 0x45 && // E
44+
data[10] === 0x42 && // B
45+
data[11] === 0x50 // P
46+
) {
47+
return 'image/webp';
48+
}
49+
50+
throw new Error(
51+
`Unable to detect image format. First bytes: ${Array.from(data.slice(0, 12))
52+
.map(b => b.toString(16).padStart(2, '0'))
53+
.join(' ')}`,
54+
);
55+
}

tests/utils/imageFormat.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
import assert from 'node:assert';
7+
import {describe, it} from 'node:test';
8+
9+
import {detectImageFormat} from '../../src/utils/imageFormat.js';
10+
11+
describe('imageFormat', () => {
12+
describe('detectImageFormat', () => {
13+
it('detects PNG format', () => {
14+
// PNG magic number: 89 50 4E 47 0D 0A 1A 0A
15+
const pngData = new Uint8Array([
16+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
17+
]);
18+
const format = detectImageFormat(pngData);
19+
assert.equal(format, 'image/png');
20+
});
21+
22+
it('detects JPEG format', () => {
23+
// JPEG magic number: FF D8 FF
24+
const jpegData = new Uint8Array([
25+
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
26+
]);
27+
const format = detectImageFormat(jpegData);
28+
assert.equal(format, 'image/jpeg');
29+
});
30+
31+
it('detects WebP format', () => {
32+
// WebP magic number: RIFF ... WEBP
33+
const webpData = new Uint8Array([
34+
0x52,
35+
0x49,
36+
0x46,
37+
0x46, // RIFF
38+
0x00,
39+
0x00,
40+
0x00,
41+
0x00, // file size (placeholder)
42+
0x57,
43+
0x45,
44+
0x42,
45+
0x50, // WEBP
46+
]);
47+
const format = detectImageFormat(webpData);
48+
assert.equal(format, 'image/webp');
49+
});
50+
51+
it('throws error for data that is too small', () => {
52+
const smallData = new Uint8Array([0x89, 0x50]);
53+
assert.throws(
54+
() => detectImageFormat(smallData),
55+
/Image data too small to detect format/,
56+
);
57+
});
58+
59+
it('throws error for unknown format', () => {
60+
const unknownData = new Uint8Array([
61+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
62+
]);
63+
assert.throws(
64+
() => detectImageFormat(unknownData),
65+
/Unable to detect image format/,
66+
);
67+
});
68+
69+
it('works with Buffer objects', () => {
70+
const pngBuffer = Buffer.from([
71+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
72+
]);
73+
const format = detectImageFormat(pngBuffer);
74+
assert.equal(format, 'image/png');
75+
});
76+
});
77+
});

0 commit comments

Comments
 (0)