Skip to main content
Glama

Figma MCP Server

MIT License
106,929
10,969
  • Linux
  • Apple
image-processing.ts7.56 kB
import fs from "fs"; import path from "path"; import sharp from "sharp"; import type { Transform } from "@figma/rest-api-spec"; /** * Apply crop transform to an image based on Figma's transformation matrix * @param imagePath - Path to the original image file * @param cropTransform - Figma transform matrix [[scaleX, skewX, translateX], [skewY, scaleY, translateY]] * @returns Promise<string> - Path to the cropped image */ export async function applyCropTransform( imagePath: string, cropTransform: Transform, ): Promise<string> { const { Logger } = await import("./logger.js"); try { // Extract transform values const scaleX = cropTransform[0]?.[0] ?? 1; const skewX = cropTransform[0]?.[1] ?? 0; const translateX = cropTransform[0]?.[2] ?? 0; const skewY = cropTransform[1]?.[0] ?? 0; const scaleY = cropTransform[1]?.[1] ?? 1; const translateY = cropTransform[1]?.[2] ?? 0; // Load the image and get metadata const image = sharp(imagePath); const metadata = await image.metadata(); if (!metadata.width || !metadata.height) { throw new Error(`Could not get image dimensions for ${imagePath}`); } const { width, height } = metadata; // Calculate crop region based on transform matrix // Figma's transform matrix represents how the image is positioned within its container // We need to extract the visible portion based on the scaling and translation // The transform matrix defines the visible area as: // - scaleX/scaleY: how much of the original image is visible (0-1) // - translateX/translateY: offset of the visible area (0-1, relative to image size) const cropLeft = Math.max(0, Math.round(translateX * width)); const cropTop = Math.max(0, Math.round(translateY * height)); const cropWidth = Math.min(width - cropLeft, Math.round(scaleX * width)); const cropHeight = Math.min(height - cropTop, Math.round(scaleY * height)); // Validate crop dimensions if (cropWidth <= 0 || cropHeight <= 0) { Logger.log(`Invalid crop dimensions for ${imagePath}, using original image`); return imagePath; } // Overwrite the original file with the cropped version const tempPath = imagePath + ".tmp"; // Apply crop transformation to temporary file first await image .extract({ left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight, }) .toFile(tempPath); // Replace original file with cropped version fs.renameSync(tempPath, imagePath); Logger.log(`Cropped image saved (overwritten): ${imagePath}`); Logger.log( `Crop region: ${cropLeft}, ${cropTop}, ${cropWidth}x${cropHeight} from ${width}x${height}`, ); return imagePath; } catch (error) { Logger.error(`Error cropping image ${imagePath}:`, error); // Return original path if cropping fails return imagePath; } } /** * Get image dimensions from a file * @param imagePath - Path to the image file * @returns Promise<{width: number, height: number}> */ export async function getImageDimensions(imagePath: string): Promise<{ width: number; height: number; }> { const { Logger } = await import("./logger.js"); try { const metadata = await sharp(imagePath).metadata(); if (!metadata.width || !metadata.height) { throw new Error(`Could not get image dimensions for ${imagePath}`); } return { width: metadata.width, height: metadata.height, }; } catch (error) { Logger.error(`Error getting image dimensions for ${imagePath}:`, error); // Return default dimensions if reading fails return { width: 1000, height: 1000 }; } } export type ImageProcessingResult = { filePath: string; originalDimensions: { width: number; height: number }; finalDimensions: { width: number; height: number }; wasCropped: boolean; cropRegion?: { left: number; top: number; width: number; height: number }; cssVariables?: string; processingLog: string[]; }; /** * Enhanced image download with post-processing * @param fileName - The filename to save as * @param localPath - The local path to save to * @param imageUrl - Image URL * @param needsCropping - Whether to apply crop transform * @param cropTransform - Transform matrix for cropping * @param requiresImageDimensions - Whether to generate dimension metadata * @returns Promise<ImageProcessingResult> - Detailed processing information */ export async function downloadAndProcessImage( fileName: string, localPath: string, imageUrl: string, needsCropping: boolean = false, cropTransform?: Transform, requiresImageDimensions: boolean = false, ): Promise<ImageProcessingResult> { const { Logger } = await import("./logger.js"); const processingLog: string[] = []; // First download the original image const { downloadFigmaImage } = await import("./common.js"); const originalPath = await downloadFigmaImage(fileName, localPath, imageUrl); Logger.log(`Downloaded original image: ${originalPath}`); // Get original dimensions before any processing const originalDimensions = await getImageDimensions(originalPath); Logger.log(`Original dimensions: ${originalDimensions.width}x${originalDimensions.height}`); let finalPath = originalPath; let wasCropped = false; let cropRegion: { left: number; top: number; width: number; height: number } | undefined; // Apply crop transform if needed if (needsCropping && cropTransform) { Logger.log("Applying crop transform..."); // Extract crop region info before applying transform const scaleX = cropTransform[0]?.[0] ?? 1; const scaleY = cropTransform[1]?.[1] ?? 1; const translateX = cropTransform[0]?.[2] ?? 0; const translateY = cropTransform[1]?.[2] ?? 0; const cropLeft = Math.max(0, Math.round(translateX * originalDimensions.width)); const cropTop = Math.max(0, Math.round(translateY * originalDimensions.height)); const cropWidth = Math.min( originalDimensions.width - cropLeft, Math.round(scaleX * originalDimensions.width), ); const cropHeight = Math.min( originalDimensions.height - cropTop, Math.round(scaleY * originalDimensions.height), ); if (cropWidth > 0 && cropHeight > 0) { cropRegion = { left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight }; finalPath = await applyCropTransform(originalPath, cropTransform); wasCropped = true; Logger.log(`Cropped to region: ${cropLeft}, ${cropTop}, ${cropWidth}x${cropHeight}`); } else { Logger.log("Invalid crop dimensions, keeping original image"); } } // Get final dimensions after processing const finalDimensions = await getImageDimensions(finalPath); Logger.log(`Final dimensions: ${finalDimensions.width}x${finalDimensions.height}`); // Generate CSS variables if required (for TILE mode) let cssVariables: string | undefined; if (requiresImageDimensions) { cssVariables = generateImageCSSVariables(finalDimensions); } return { filePath: finalPath, originalDimensions, finalDimensions, wasCropped, cropRegion, cssVariables, processingLog, }; } /** * Create CSS custom properties for image dimensions * @param imagePath - Path to the image file * @returns Promise<string> - CSS custom properties */ export function generateImageCSSVariables({ width, height, }: { width: number; height: number; }): string { return `--original-width: ${width}px; --original-height: ${height}px;`; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/GLips/Figma-Context-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server