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
13 changes: 13 additions & 0 deletions src/handlers/tools.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ export function setupTools(server: Server, provider: AutomationProvider): void {
default: { width: 1280, fit: 'contain' },
description: 'Resize options for the screenshot',
},
grid: {
oneOf: [{ type: 'boolean' }, { type: 'number', minimum: 10, maximum: 500 }],
default: false,
description:
'Draw coordinate grid overlay (true for 100px spacing, or specify pixel spacing)',
},
gridTransparency: {
type: 'number',
minimum: 0,
maximum: 100,
default: 50,
description: 'Grid line transparency (0 = fully transparent, 100 = fully opaque)',
},
},
},
},
Expand Down
56 changes: 53 additions & 3 deletions src/providers/keysender/screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ export class KeysenderScreenAutomation implements ScreenAutomation {
handle: number;
};

this.logger.warn(`Using fallback window "${fallbackWindow.title}" with default view values`);
this.logger.warn(
`Using fallback window "${fallbackWindow.title}" with default view values`,
);

return {
window: fallbackWindow,
Expand Down Expand Up @@ -427,7 +429,7 @@ export class KeysenderScreenAutomation implements ScreenAutomation {
(Math.abs(updatedView.width - width) > 20 || Math.abs(updatedView.height - height) > 20)
) {
this.logger.warn(
`Resize may not have been successful. Requested: ${width}x${height}, Got: ${updatedView.width}x${updatedView.height}`
`Resize may not have been successful. Requested: ${width}x${height}, Got: ${updatedView.width}x${updatedView.height}`,
);
} else if (
operationType === 'reposition' &&
Expand All @@ -436,7 +438,7 @@ export class KeysenderScreenAutomation implements ScreenAutomation {
(Math.abs(updatedView.x - x) > 20 || Math.abs(updatedView.y - y) > 20)
) {
this.logger.warn(
`Repositioning may not have been successful. Requested: (${x}, ${y}), Got: (${updatedView.x}, ${updatedView.y})`
`Repositioning may not have been successful. Requested: (${x}, ${y}), Got: (${updatedView.x}, ${updatedView.y})`,
);
}
} catch (viewError) {
Expand Down Expand Up @@ -599,6 +601,54 @@ export class KeysenderScreenAutomation implements ScreenAutomation {
});
}

// Draw grid overlay if requested
if (mergedOptions.grid) {
const gridSpacing = typeof mergedOptions.grid === 'number' ? mergedOptions.grid : 100;

// Calculate final image dimensions from resize settings
const targetWidth = mergedOptions.resize?.width || 1280;
const imgWidth = Math.min(width, targetWidth);
const imgHeight = Math.round(height * (imgWidth / width));

// Get window position offset to show true screen coordinates
let offsetX = 0;
let offsetY = 0;
try {
const viewInfo = this.hardware.workwindow.getView();
offsetX = viewInfo.x || 0;
offsetY = viewInfo.y || 0;
} catch {
// If we can't get window position, use 0 offset (full screen capture)
}

// Calculate scale factor (image pixels to screen pixels)
const scaleX = width / imgWidth;
const scaleY = height / imgHeight;

// Calculate grid opacity from transparency (0-100 -> 0-1)
const gridOpacity = (mergedOptions.gridTransparency ?? 50) / 100;
const textOpacity = Math.min(1, gridOpacity + 0.3); // Text slightly more visible

// Build SVG grid with screen coordinates (accounting for window offset and scale)
let svgLines = '';
for (let x = gridSpacing; x < imgWidth; x += gridSpacing) {
const screenX = Math.round(offsetX + x * scaleX);
svgLines += `<line x1="${x}" y1="0" x2="${x}" y2="${imgHeight}" stroke="rgba(255,0,0,${gridOpacity})" stroke-width="1"/>`;
svgLines += `<text x="${x + 2}" y="12" font-size="10" fill="rgba(255,0,0,${textOpacity})">${screenX}</text>`;
}
for (let y = gridSpacing; y < imgHeight; y += gridSpacing) {
const screenY = Math.round(offsetY + y * scaleY);
svgLines += `<line x1="0" y1="${y}" x2="${imgWidth}" y2="${y}" stroke="rgba(255,0,0,${gridOpacity})" stroke-width="1"/>`;
svgLines += `<text x="2" y="${y - 2}" font-size="10" fill="rgba(255,0,0,${textOpacity})">${screenY}</text>`;
}

const svgOverlay = Buffer.from(
`<svg width="${imgWidth}" height="${imgHeight}">${svgLines}</svg>`,
);

pipeline = pipeline.composite([{ input: svgOverlay, top: 0, left: 0 }]);
}

// Apply appropriate format-specific compression
if (mergedOptions.format === 'jpeg') {
pipeline = pipeline.jpeg({
Expand Down
54 changes: 54 additions & 0 deletions src/tools/screenshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,59 @@ describe('Screenshot Functions', () => {
message: 'Failed to capture screenshot: Capture failed',
});
});

it('should pass grid options to provider', async () => {
// Setup mock provider
const mockProvider = {
screen: {
getScreenshot: vi.fn().mockResolvedValue({
success: true,
message: 'Screenshot captured successfully',
content: [
{
type: 'image',
data: 'test-image-with-grid',
mimeType: 'image/png',
},
],
}),
},
};

vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any);

const options = {
grid: true,
gridTransparency: 75,
};

const result = await getScreenshot(options);

expect(mockProvider.screen.getScreenshot).toHaveBeenCalledWith(options);
expect(result.success).toBe(true);
});

it('should pass grid spacing as number to provider', async () => {
const mockProvider = {
screen: {
getScreenshot: vi.fn().mockResolvedValue({
success: true,
message: 'Screenshot captured successfully',
content: [{ type: 'image', data: 'test', mimeType: 'image/png' }],
}),
},
};

vi.mocked(createAutomationProvider).mockReturnValue(mockProvider as any);

const options = {
grid: 200, // Custom grid spacing
gridTransparency: 30,
};

await getScreenshot(options);

expect(mockProvider.screen.getScreenshot).toHaveBeenCalledWith(options);
});
});
});
2 changes: 2 additions & 0 deletions src/tools/validation.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ export const ScreenshotOptionsSchema = z.object({
grayscale: z.boolean().optional(),
resize: ScreenshotResizeSchema.optional(),
compressionLevel: z.number().int().min(0).max(9).optional(),
grid: z.union([z.boolean(), z.number().int().min(10).max(500)]).optional(),
gridTransparency: z.number().int().min(0).max(100).optional(),
});

/**
Expand Down
2 changes: 2 additions & 0 deletions src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,6 @@ export interface ScreenshotOptions {
fit?: 'contain' | 'cover' | 'fill' | 'inside' | 'outside'; // Resize fit option
};
compressionLevel?: number; // PNG compression level (0-9), only used if format is 'png'
grid?: boolean | number; // Draw coordinate grid overlay (true for 100px, or specify spacing)
gridTransparency?: number; // Grid line transparency (0-100, default 50)
}