diff --git a/mcp-server/src/tools/ImportImageTool.ts b/mcp-server/src/tools/ImportImageTool.ts index 834692e..7581aaf 100644 --- a/mcp-server/src/tools/ImportImageTool.ts +++ b/mcp-server/src/tools/ImportImageTool.ts @@ -44,7 +44,7 @@ export class ImportImageArgs { } /** - * Tool for importing an image from the local file system into Penpot + * Tool for importing a raster image from the local file system into Penpot */ export class ImportImageTool extends Tool { /** @@ -56,7 +56,6 @@ export class ImportImageTool extends Tool { ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp", - ".bmp": "image/bmp", }; /** @@ -78,7 +77,7 @@ export class ImportImageTool extends Tool { "that uses the image as a fill. The rectangle has the image's original proportions by default. " + "Optionally accepts position (x, y) and dimensions (width, height) parameters. " + "If only one dimension is provided, the other is calculated to maintain the image's aspect ratio. " + - "Supported image formats: JPG, PNG, GIF, WEBP, and BMP." + "Supported formats: JPEG, PNG, GIF, WEBP." ); } @@ -107,76 +106,18 @@ export class ImportImageTool extends Tool { // generate and execute JavaScript code to import the image const fileName = path.basename(args.filePath); - const code = this.generateImportCode(fileName, base64Data, mimeType, args); - const task = new ExecuteCodePluginTask({ code: code }); - await this.mcpServer.pluginBridge.executePluginTask(task); - - return new TextResponse(`Image imported successfully from ${args.filePath}`); - } - - /** - * Generates the JavaScript code to import the image in the Penpot plugin context. - * - * @param fileName - The name of the file - * @param base64Data - The base64-encoded image data - * @param mimeType - The MIME type of the image - * @param args - The tool arguments containing position and dimension parameters - */ - protected generateImportCode( - fileName: string, - base64Data: string, - mimeType: string, - args: ImportImageArgs - ): string { - // escape the base64 data for use in a JavaScript string const escapedBase64 = base64Data.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); const escapedFileName = fileName.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); + const code = ` + const rectangle = await penpotUtils.importImage( + '${escapedBase64}', '${mimeType}', '${escapedFileName}', + ${args.x ?? "undefined"}, ${args.y ?? "undefined"}, + ${args.width ?? "undefined"}, ${args.height ?? "undefined"}); + return { shapeId: rectangle.id }; + `; + const task = new ExecuteCodePluginTask({ code: code }); + const executionResult = await this.mcpServer.pluginBridge.executePluginTask(task); - return ` - // convert base64 to Uint8Array - const base64 = '${escapedBase64}'; - const bytes = penpotUtils.atob(base64); - - // upload the image data to Penpot - const imageData = await penpot.uploadMediaData('${escapedFileName}', bytes, '${mimeType}'); - - // create a rectangle shape - const rect = penpot.createRectangle(); - rect.name = '${escapedFileName}'; - - // calculate dimensions - let rectWidth, rectHeight; - const hasWidth = ${args.width !== undefined}; - const hasHeight = ${args.height !== undefined}; - - if (hasWidth && hasHeight) { - // both width and height provided - use them directly - rectWidth = ${args.width ?? 0}; - rectHeight = ${args.height ?? 0}; - } else if (hasWidth) { - // only width provided - maintain aspect ratio - rectWidth = ${args.width ?? 0}; - rectHeight = rectWidth * (imageData.height / imageData.width); - } else if (hasHeight) { - // only height provided - maintain aspect ratio - rectHeight = ${args.height ?? 0}; - rectWidth = rectHeight * (imageData.width / imageData.height); - } else { - // neither provided - use original dimensions - rectWidth = imageData.width; - rectHeight = imageData.height; - } - - // set rectangle dimensions - rect.resize(rectWidth, rectHeight); - - // set position if provided - ${args.x !== undefined ? `rect.x = ${args.x};` : ""} - ${args.y !== undefined ? `rect.y = ${args.y};` : ""} - - // apply the image as a fill - rect.fills = [{ fillOpacity: 1, fillImage: imageData }]; - - return { shapeId: rect.id };`; + return new TextResponse(JSON.stringify(executionResult.data?.result, null, 2)); } } diff --git a/penpot-plugin/src/PenpotUtils.ts b/penpot-plugin/src/PenpotUtils.ts index f737b20..f5627ee 100644 --- a/penpot-plugin/src/PenpotUtils.ts +++ b/penpot-plugin/src/PenpotUtils.ts @@ -1,4 +1,4 @@ -import { Page, Shape } from "@penpot/plugin-types"; +import { Page, Rectangle, Shape } from "@penpot/plugin-types"; export class PenpotUtils { /** @@ -177,4 +177,79 @@ export class PenpotUtils { return bytes; } + + /** + * Imports an image from base64 data into the Penpot design as a Rectangle shape filled with the image. + * The rectangle has the image's original proportions by default. + * Optionally accepts position (x, y) and dimensions (width, height) parameters. + * If only one dimension is provided, the other is calculated to maintain the image's aspect ratio. + * + * This function is used internally by the ImportImageTool in the MCP server. + * + * @param base64 - The base64-encoded image data + * @param mimeType - The MIME type of the image (e.g., "image/png") + * @param name - The name to assign to the newly created rectangle shape + * @param x - The x-coordinate for positioning the rectangle (optional) + * @param y - The y-coordinate for positioning the rectangle (optional) + * @param width - The desired width of the rectangle (optional) + * @param height - The desired height of the rectangle (optional) + */ + public static async importImage( + base64: string, + mimeType: string, + name: string, + x: number | undefined, + y: number | undefined, + width: number | undefined, + height: number | undefined + ): Promise { + // convert base64 to Uint8Array + const bytes = PenpotUtils.atob(base64); + + // upload the image data to Penpot + const imageData = await penpot.uploadMediaData(name, bytes, mimeType); + + // create a rectangle shape + const rect = penpot.createRectangle(); + rect.name = "${escapedFileName}"; + + // calculate dimensions + let rectWidth, rectHeight; + const hasWidth = width !== undefined; + const hasHeight = height !== undefined; + + if (hasWidth && hasHeight) { + // both width and height provided - use them directly + rectWidth = width; + rectHeight = height; + } else if (hasWidth) { + // only width provided - maintain aspect ratio + rectWidth = width; + rectHeight = rectWidth * (imageData.height / imageData.width); + } else if (hasHeight) { + // only height provided - maintain aspect ratio + rectHeight = height; + rectWidth = rectHeight * (imageData.width / imageData.height); + } else { + // neither provided - use original dimensions + rectWidth = imageData.width; + rectHeight = imageData.height; + } + + // set rectangle dimensions + rect.resize(rectWidth, rectHeight); + + // set position if provided + if (x !== undefined) { + rect.x = x; + } + if (y !== undefined) { + rect.y = y; + } + + // apply the image as a fill + rect.fills = [{ fillOpacity: 1, fillImage: imageData }]; + + return rect; + } }