Place Image
photopea_place_imagePlace an image from a URL or local file into the active document. Creates a new layer; optionally set position (x, y) and resize (width, height) while preserving aspect ratio.
Instructions
Place an image into the active document from a URL or local file path. Creates a new layer with the placed image as the active layer. Use width/height to resize while preserving aspect ratio, or x/y to position the layer.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| source | Yes | URL or absolute local file path of the image to place | |
| x | No | X position offset in pixels from the left edge | |
| y | No | Y position offset in pixels from the top edge | |
| width | No | Resize to this width in pixels (preserves aspect ratio if only one dimension is set) | |
| height | No | Resize to this height in pixels (preserves aspect ratio if only one dimension is set) | |
| name | No | Display name for the placed layer in the layers panel |
Implementation Reference
- src/tools/image.ts:36-93 (handler)The async handler function that executes the photopea_place_image tool logic. It fetches file data (URL or local), loads it into Photopea via bridge.loadFile, duplicates the layer into the target document via copy/paste, optionally renames, resizes, and positions the layer, then confirms success.
const { source } = params; bridge.sendActivity({ type: "activity", id: "", tool: "place_image", summary: `Place image: ${source}` }); // Step 1: Fetch the file data (server-side for both URLs and local files) // This avoids Photopea's async app.open(url) which sends "done" before the file loads. let fileData: Buffer; try { fileData = isUrl(source) ? await fetchUrlToBuffer(source) : await readLocalFile(source); } catch (err) { return { isError: true, content: [{ type: "text" as const, text: (err as Error).message }] }; } // Step 2: Send ArrayBuffer to Photopea (opens as new document synchronously) const filename = source.split("/").pop() || "image"; const loadResult = await bridge.loadFile(fileData, filename); if (!loadResult.success) return { isError: true, content: [{ type: "text" as const, text: loadResult.error || "Failed to load image" }] }; // Step 3: Duplicate layer into target doc, close source, position in target { const lines = [ `var _srcDoc = app.activeDocument;`, // Copy merged preserves transparency (unlike flatten/mergeVisible) `_srcDoc.selection.selectAll();`, `_srcDoc.selection.copy(true);`, `_srcDoc.close(2);`, `app.activeDocument.paste();`, ]; const safeName = params.name ? escapeString(params.name) : ""; // Helper to extract numeric value from bounds (may be UnitValue objects or raw numbers) lines.push(`function _bv(v) { return typeof v === 'object' && v !== null ? v.value || v.L || 0 : v; }`); if (safeName) lines.push(`app.activeDocument.activeLayer.name = '${safeName}';`); if (params.width || params.height) { lines.push(`var _b = app.activeDocument.activeLayer.bounds;`); lines.push(`var _cw = _bv(_b[2]) - _bv(_b[0]);`); lines.push(`var _ch = _bv(_b[3]) - _bv(_b[1]);`); if (params.width && params.height) { // Fit within box, preserving aspect ratio lines.push(`var _scale = Math.min(${params.width} / _cw, ${params.height} / _ch);`); } else if (params.width) { lines.push(`var _scale = ${params.width} / _cw;`); } else { lines.push(`var _scale = ${params.height} / _ch;`); } lines.push(`if (_cw > 0 && _ch > 0) { app.activeDocument.activeLayer.resize(_scale * 100, _scale * 100); }`); } if (params.x !== undefined || params.y !== undefined) { const xPos = params.x ?? 0; const yPos = params.y ?? 0; lines.push(`var _b2 = app.activeDocument.activeLayer.bounds;`); lines.push(`app.activeDocument.activeLayer.translate(${xPos} - _bv(_b2[0]), ${yPos} - _bv(_b2[1]));`); } lines.push(`app.echoToOE('ok');`); const mergeResult = await bridge.executeScript(lines.join("\n")); if (!mergeResult.success) return { isError: true, content: [{ type: "text" as const, text: mergeResult.error || "Failed to place image into document" }] }; } return { content: [{ type: "text" as const, text: `Image placed: ${source}` }] }; - src/tools/image.ts:26-33 (schema)The input schema for photopea_place_image, defining parameters: source (required URL or file path), x/y (optional position offset), width/height (optional resize dimensions), and name (optional layer name).
inputSchema: { source: z.string().describe("URL or absolute local file path of the image to place"), x: z.number().optional().describe("X position offset in pixels from the left edge"), y: z.number().optional().describe("Y position offset in pixels from the top edge"), width: z.number().positive().optional().describe("Resize to this width in pixels (preserves aspect ratio if only one dimension is set)"), height: z.number().positive().optional().describe("Resize to this height in pixels (preserves aspect ratio if only one dimension is set)"), name: z.string().optional().describe("Display name for the placed layer in the layers panel"), }, - src/tools/image.ts:23-23 (registration)The MCP tool registration using server.registerTool('photopea_place_image', ...) with title, description, inputSchema, annotations, and handler callback.
server.registerTool("photopea_place_image", { - src/tools/image.ts:21-95 (registration)The registerImageTools function which registers all image tools including photopea_place_image. Exported and called from src/server.ts.
export function registerImageTools(server: McpServer, bridge: PhotopeaBridge): void { // 19. photopea_place_image server.registerTool("photopea_place_image", { title: "Place Image", description: "Place an image into the active document from a URL or local file path. Creates a new layer with the placed image as the active layer. Use width/height to resize while preserving aspect ratio, or x/y to position the layer.", inputSchema: { source: z.string().describe("URL or absolute local file path of the image to place"), x: z.number().optional().describe("X position offset in pixels from the left edge"), y: z.number().optional().describe("Y position offset in pixels from the top edge"), width: z.number().positive().optional().describe("Resize to this width in pixels (preserves aspect ratio if only one dimension is set)"), height: z.number().positive().optional().describe("Resize to this height in pixels (preserves aspect ratio if only one dimension is set)"), name: z.string().optional().describe("Display name for the placed layer in the layers panel"), }, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, }, async (params) => { const { source } = params; bridge.sendActivity({ type: "activity", id: "", tool: "place_image", summary: `Place image: ${source}` }); // Step 1: Fetch the file data (server-side for both URLs and local files) // This avoids Photopea's async app.open(url) which sends "done" before the file loads. let fileData: Buffer; try { fileData = isUrl(source) ? await fetchUrlToBuffer(source) : await readLocalFile(source); } catch (err) { return { isError: true, content: [{ type: "text" as const, text: (err as Error).message }] }; } // Step 2: Send ArrayBuffer to Photopea (opens as new document synchronously) const filename = source.split("/").pop() || "image"; const loadResult = await bridge.loadFile(fileData, filename); if (!loadResult.success) return { isError: true, content: [{ type: "text" as const, text: loadResult.error || "Failed to load image" }] }; // Step 3: Duplicate layer into target doc, close source, position in target { const lines = [ `var _srcDoc = app.activeDocument;`, // Copy merged preserves transparency (unlike flatten/mergeVisible) `_srcDoc.selection.selectAll();`, `_srcDoc.selection.copy(true);`, `_srcDoc.close(2);`, `app.activeDocument.paste();`, ]; const safeName = params.name ? escapeString(params.name) : ""; // Helper to extract numeric value from bounds (may be UnitValue objects or raw numbers) lines.push(`function _bv(v) { return typeof v === 'object' && v !== null ? v.value || v.L || 0 : v; }`); if (safeName) lines.push(`app.activeDocument.activeLayer.name = '${safeName}';`); if (params.width || params.height) { lines.push(`var _b = app.activeDocument.activeLayer.bounds;`); lines.push(`var _cw = _bv(_b[2]) - _bv(_b[0]);`); lines.push(`var _ch = _bv(_b[3]) - _bv(_b[1]);`); if (params.width && params.height) { // Fit within box, preserving aspect ratio lines.push(`var _scale = Math.min(${params.width} / _cw, ${params.height} / _ch);`); } else if (params.width) { lines.push(`var _scale = ${params.width} / _cw;`); } else { lines.push(`var _scale = ${params.height} / _ch;`); } lines.push(`if (_cw > 0 && _ch > 0) { app.activeDocument.activeLayer.resize(_scale * 100, _scale * 100); }`); } if (params.x !== undefined || params.y !== undefined) { const xPos = params.x ?? 0; const yPos = params.y ?? 0; lines.push(`var _b2 = app.activeDocument.activeLayer.bounds;`); lines.push(`app.activeDocument.activeLayer.translate(${xPos} - _bv(_b2[0]), ${yPos} - _bv(_b2[1]));`); } lines.push(`app.echoToOE('ok');`); const mergeResult = await bridge.executeScript(lines.join("\n")); if (!mergeResult.success) return { isError: true, content: [{ type: "text" as const, text: mergeResult.error || "Failed to place image into document" }] }; } return { content: [{ type: "text" as const, text: `Image placed: ${source}` }] }; }); - src/bridge/types.ts:186-192 (helper)The PlaceImageParams interface defining the typed parameter structure for the place_image operation: source, x, y, width, height, name.
export interface PlaceImageParams { source: string; x?: number; y?: number; width?: number; height?: number; name?: string;