UNPKG

@prodbirdy/mockup-generator

Version:

Serverless-optimized TypeScript SDK for generating high-quality product mockups from PSD templates

628 lines (541 loc) 16.6 kB
/** * MockupSDK - Main SDK class for mockup generation * Provides a clean, simple API for serverless environments */ import { EventEmitter } from "events"; import { initializeCanvas } from "ag-psd"; import { createCanvas } from "canvas"; import type { MockupSDKConfig, MockupSDKMode, MockupGenerationConfig, BatchMockupConfig, MockupResult, BatchMockupResult, UploadOptions, UploadResult, StorageConfig, ExportResult, } from "./types"; import { MockupSDKError, ValidationError, StorageError, ExportError, } from "./types"; import { MockupEngine } from "./MockupEngine"; import { HeadlessPhotopeaEngine } from "./HeadlessPhotopeaEngine"; import { StorageManager } from "./StorageManager"; import { validateGenerationConfig, validateBatchConfig, validateUploadOptions, } from "./validation"; /** * Mode presets for common configurations */ const MODE_PRESETS: Record<MockupSDKMode, Partial<MockupSDKConfig>> = { serverless: { headless: true, timeout: 45000, verbose: false, }, development: { headless: false, timeout: 120000, verbose: true, }, production: { headless: true, timeout: 60000, verbose: false, }, auto: { // Will be resolved based on environment detection headless: true, timeout: 60000, verbose: false, }, }; export class MockupSDK extends EventEmitter { private mockupEngine: MockupEngine; private exportEngine: HeadlessPhotopeaEngine | null = null; private storageManager: StorageManager | null; private config: MockupSDKConfig; private constructor(config: MockupSDKConfig = {}) { super(); // Initialize canvas for ag-psd before any PSD operations this.initializeCanvasSupport(); // Resolve configuration with mode presets this.config = this.resolveConfig(config); // Initialize components - storage is now optional this.storageManager = this.config.storage ? new StorageManager(this.config.storage) : null; this.mockupEngine = new MockupEngine(this.config); } /** * Factory method to create and initialize MockupSDK */ static async create(config: MockupSDKConfig = {}): Promise<MockupSDK> { const instance = new MockupSDK(config); await instance.initialize(); return instance; } /** * Initialize async components */ private async initialize(): Promise<void> { // Initialize HeadlessPhotopeaEngine this.exportEngine = await HeadlessPhotopeaEngine.create(this.config); // Forward events this.setupEventForwarding(); } /** * Generate a single mockup from configuration * * @param config - Mockup generation configuration * @returns Promise<MockupResult> */ async generate(config: MockupGenerationConfig): Promise<MockupResult> { if (!this.exportEngine) { throw new MockupSDKError( "MockupSDK not initialized. Use MockupSDK.create() instead of constructor.", "NOT_INITIALIZED" ); } try { // Validate configuration validateGenerationConfig(config); this.emit("progress", "validation", 10); const startTime = Date.now(); // Step 1: Generate PSD with replacements this.emit("progress", "generating-psd", 20); const psdBuffer = await this.mockupEngine.generatePSD( config.psd, config.replacements ); this.emit("psd-generated", psdBuffer); // Step 2: Export to requested formats directly from buffer (optimal for generated PSDs) let exportResults: ExportResult[] = []; if (config.exports && config.exports.length > 0) { this.emit("progress", "exporting", 60); if (config.exports.length === 1) { // Single export - use individual method const exportOption = config.exports[0]; this.emit("export-started", exportOption?.format); const result = await this.exportEngine.exportFormat( psdBuffer, exportOption! ); exportResults.push(result); this.emit("export-completed", result); } else { // Multiple exports - use optimized batch method (single browser session) for (const exportOption of config.exports) { this.emit("export-started", exportOption.format); } exportResults = await this.exportEngine.exportMultipleFormats( psdBuffer, config.exports ); for (const result of exportResults) { this.emit("export-completed", result); } } } this.emit("progress", "completed", 100); const processingTime = Date.now() - startTime; return { psd: psdBuffer, exports: exportResults, metadata: { processingTime, sourceDimensions: await this.getPSDDimensions(config.psd), replacementsCount: config.replacements.length, }, }; } catch (error) { const sdkError = this.wrapError(error); this.emit("error", sdkError); throw sdkError; } } /** * Generate multiple mockup variations from a batch configuration * * @param config - Batch mockup configuration * @returns Promise<BatchMockupResult> */ async generateBatch(config: BatchMockupConfig): Promise<BatchMockupResult> { try { // Validate configuration validateBatchConfig(config); this.emit("progress", "batch-validation", 5); const batchStartTime = Date.now(); const results = []; const errors = []; // Process each variation for (const [index, variation] of config.variations.entries()) { try { this.emit( "progress", `batch-item-${variation.id}`, (index / config.variations.length) * 90 + 10 ); const mockupConfig: MockupGenerationConfig = { psd: config.template, replacements: variation.replacements, exports: variation.exports, }; const result = await this.generate(mockupConfig); results.push({ id: variation.id, psd: result.psd, exports: result.exports, generationMetadata: result.metadata, variationMetadata: variation.metadata, }); } catch (error) { errors.push({ id: variation.id, error: error instanceof Error ? error.message : String(error), }); if (this.config.verbose) { console.error( `Failed to generate variation ${variation.id}:`, error ); } } } this.emit("progress", "batch-completed", 100); const totalProcessingTime = Date.now() - batchStartTime; return { results, batchMetadata: { totalProcessingTime, successCount: results.length, failureCount: errors.length, errors, }, }; } catch (error) { const sdkError = this.wrapError(error); this.emit("error", sdkError); throw sdkError; } } /** * Upload mockup results to storage * * @param result - Mockup result to upload * @param options - Upload options * @returns Promise<UploadResult> */ async upload( result: MockupResult, options: UploadOptions = {} ): Promise<UploadResult> { try { validateUploadOptions(options); const startTime = Date.now(); // Get storage manager (from options or SDK config) const storage = this.getStorageManager(options.storage); const uploadPSD = options.uploadPSD ?? true; const uploadExports = options.uploadExports ?? true; const prefix = options.prefix || "mockups"; let psdUrl: string | undefined; const exportUrls: Record<string, string> = {}; const sizes: Record<string, number> = {}; // Upload PSD if requested if (uploadPSD) { this.emit("progress", "uploading-psd", 20); psdUrl = await storage.uploadPSD(result.psd, prefix); sizes.psd = result.psd.length; } // Upload exports if requested if (uploadExports) { for (const [index, exportResult] of result.exports.entries()) { this.emit( "progress", `uploading-${exportResult.format}`, 20 + (index / result.exports.length) * 60 ); const uploadResult = await storage.uploadExport( exportResult.buffer, exportResult.format, `${prefix}/exports` ); exportUrls[exportResult.format] = uploadResult.url; sizes[exportResult.format] = exportResult.size; } } const uploadTime = Date.now() - startTime; return { psdUrl, exportUrls, metadata: { uploadTime, sizes, }, }; } catch (error) { const sdkError = this.wrapError(error); this.emit("error", sdkError); throw sdkError; } } /** * Upload batch mockup results to storage * * @param batchResult - Batch result to upload * @param options - Upload options * @returns Promise<UploadResult[]> */ async uploadBatch( batchResult: BatchMockupResult, options: UploadOptions = {} ): Promise<UploadResult[]> { const results: UploadResult[] = []; for (const [index, result] of batchResult.results.entries()) { try { this.emit( "progress", `batch-upload-${result.id}`, (index / batchResult.results.length) * 100 ); const uploadOptions = { ...options, prefix: options.prefix ? `${options.prefix}/${result.id}` : result.id, }; const uploadResult = await this.upload(result, uploadOptions); results.push(uploadResult); } catch (error) { if (this.config.verbose) { console.error(`Failed to upload result ${result.id}:`, error); } // Continue with other uploads } } return results; } /** * Utility method to inspect a PSD structure */ async analyze(psd: string | Buffer): Promise<{ dimensions: { width: number; height: number }; smartObjects: Array<{ name: string; dimensions: { width: number; height: number }; path: string[]; }>; layers: number; }> { return this.mockupEngine.analyzePSD(psd); } /** * Get storage info (buckets, usage, etc.) */ async getStorageInfo(): Promise<{ bucket: string; region?: string; connected: boolean; }> { if (!this.storageManager) { return { bucket: "", connected: false }; } return this.storageManager.getInfo(); } /** * Cleanup resources (important for serverless environments) */ async cleanup(): Promise<void> { await this.exportEngine?.cleanup(); await this.storageManager?.cleanup?.(); } // Private methods /** * Initialize canvas support for ag-psd * This must be called before any PSD operations */ private initializeCanvasSupport(): void { try { initializeCanvas((width: number, height: number) => { const canvas = createCanvas(width, height); canvas.width = width; canvas.height = height; return canvas as any; }); } catch (error) { throw new MockupSDKError( "Failed to initialize canvas. Make sure 'canvas' package is installed: npm install canvas", "CANVAS_INIT_ERROR", error ); } } private resolveConfig(config: MockupSDKConfig): MockupSDKConfig { const mode = config.mode || "production"; // Get base preset let baseConfig = { ...MODE_PRESETS[mode] }; // Handle auto mode - detect environment if (mode === "auto") { baseConfig = this.detectEnvironmentConfig(); // Try to load storage from environment if (!config.storage) { const envStorage = this.loadStorageFromEnvironment(); if (envStorage) { baseConfig.storage = envStorage; } } } // Override with user config (user config takes precedence) const resolvedConfig = { ...baseConfig, ...config, mode, }; return resolvedConfig; } private detectEnvironmentConfig(): Partial<MockupSDKConfig> { // Check for serverless environment indicators if ( process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.VERCEL || process.env.CF_PAGES ) { return MODE_PRESETS.serverless; } // Check NODE_ENV if (process.env.NODE_ENV === "development") { return MODE_PRESETS.development; } if (process.env.NODE_ENV === "production") { return MODE_PRESETS.production; } // Default to production return MODE_PRESETS.production; } private loadStorageFromEnvironment(): StorageConfig | null { const endpoint = process.env.R2_ENDPOINT || process.env.S3_ENDPOINT; const accessKeyId = process.env.R2_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID; const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY; const bucket = process.env.R2_BUCKET || process.env.S3_BUCKET; const region = process.env.R2_REGION || process.env.AWS_REGION; if (endpoint && accessKeyId && secretAccessKey && bucket) { return { endpoint, accessKeyId, secretAccessKey, bucket, region, }; } return null; } private getStorageManager(storageConfig?: StorageConfig): StorageManager { if (storageConfig) { return new StorageManager(storageConfig); } if (this.storageManager) { return this.storageManager; } throw new StorageError( "No storage configuration available. Provide storage config in SDK constructor or upload options." ); } private async getPSDDimensions( psd: string | Buffer ): Promise<{ width: number; height: number }> { try { const analysis = await this.mockupEngine.analyzePSD(psd); return analysis.dimensions; } catch { return { width: 0, height: 0 }; // Fallback } } /** * Convenience factory methods (thin wrappers around mode-based config) */ /** * Create SDK with serverless mode preset */ static async forServerless(config: Omit<MockupSDKConfig, "mode"> = {}): Promise<MockupSDK> { return MockupSDK.create({ mode: "serverless", ...config }); } /** * Create SDK with development mode preset */ static async forDevelopment(config: Omit<MockupSDKConfig, "mode"> = {}): Promise<MockupSDK> { return MockupSDK.create({ mode: "development", ...config }); } /** * Create SDK with production mode preset */ static async forProduction(config: Omit<MockupSDKConfig, "mode"> = {}): Promise<MockupSDK> { return MockupSDK.create({ mode: "production", ...config }); } /** * Create SDK with auto mode (detects environment automatically) */ static async auto(config: Omit<MockupSDKConfig, "mode"> = {}): Promise<MockupSDK> { return MockupSDK.create({ mode: "auto", ...config }); } // Private methods private setupEventForwarding(): void { // Forward events from internal components this.mockupEngine.on("progress", (stage, progress) => this.emit("progress", stage, progress) ); this.exportEngine?.on("progress", (stage, progress) => this.emit("progress", stage, progress) ); this.storageManager?.on("progress", (stage, progress) => this.emit("progress", stage, progress) ); } private wrapError(error: any): MockupSDKError { if (error instanceof MockupSDKError) { return error; } if ( error.message?.includes("storage") || error.message?.includes("upload") ) { return new StorageError(error.message, error); } if ( error.message?.includes("export") || error.message?.includes("photopea") ) { return new ExportError(error.message, error); } if ( error.message?.includes("validation") || error.message?.includes("invalid") ) { return new ValidationError(error.message, error); } return new MockupSDKError( error.message || "Unknown error", "UNKNOWN_ERROR", error ); } } // Export types for consumers export type { MockupSDKConfig, SmartObjectReplacement, ExportOptions, MockupResult, ExportResult, MockupSDKEvents, MockupSDKError, ValidationError, StorageError, ExportError, } from "./types";