UNPKG

merge-jpg

Version:

A privacy-first client-side image merging library powered by TLDraw Canvas

1,286 lines (1,277 loc) 42.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/types/index.ts var DEFAULT_MERGE_SETTINGS, VALIDATION_CONSTRAINTS; var init_types = __esm({ "src/types/index.ts"() { "use strict"; DEFAULT_MERGE_SETTINGS = { direction: "vertical", format: "jpeg", spacing: 10, backgroundColor: "#ffffff", quality: 90, pdfPageSize: "a4" }; VALIDATION_CONSTRAINTS = { /** Maximum file size in bytes (100MB) */ MAX_FILE_SIZE: 100 * 1024 * 1024, /** Maximum number of images that can be merged at once */ MAX_FILE_COUNT: 50, /** Allowed MIME types */ ALLOWED_TYPES: ["image/jpeg", "image/jpg", "image/png"], /** Allowed file extensions */ ALLOWED_EXTENSIONS: [".jpg", ".jpeg", ".png"], /** Quality range for JPEG */ QUALITY_RANGE: { min: 10, max: 100 }, /** Spacing range in pixels */ SPACING_RANGE: { min: 0, max: 200 } }; } }); // src/utils/validation.ts function validateImages(images) { const errors = []; if (images.length === 0) { errors.push({ type: "file_count", message: "At least one image is required for merging" }); return errors; } if (images.length > VALIDATION_CONSTRAINTS.MAX_FILE_COUNT) { errors.push({ type: "file_count", message: `Maximum ${VALIDATION_CONSTRAINTS.MAX_FILE_COUNT} images allowed, got ${images.length}` }); } for (const image of images) { if (image.size > VALIDATION_CONSTRAINTS.MAX_FILE_SIZE) { errors.push({ type: "file_size", message: `File "${image.name}" is too large (${formatFileSize(image.size)}). Maximum size is ${formatFileSize(VALIDATION_CONSTRAINTS.MAX_FILE_SIZE)}`, fileName: image.name }); } if (!VALIDATION_CONSTRAINTS.ALLOWED_TYPES.includes(image.type)) { errors.push({ type: "file_type", message: `File "${image.name}" has unsupported type "${image.type}". Allowed types: ${VALIDATION_CONSTRAINTS.ALLOWED_TYPES.join(", ")}`, fileName: image.name }); } if (!image.width || !image.height || image.width <= 0 || image.height <= 0) { errors.push({ type: "processing", message: `File "${image.name}" has invalid dimensions (${image.width}x${image.height})`, fileName: image.name }); } } return errors; } function validateSettings(settings) { const errors = []; if (settings.format === "jpeg") { if (settings.quality < VALIDATION_CONSTRAINTS.QUALITY_RANGE.min || settings.quality > VALIDATION_CONSTRAINTS.QUALITY_RANGE.max) { errors.push({ type: "processing", message: `JPEG quality must be between ${VALIDATION_CONSTRAINTS.QUALITY_RANGE.min} and ${VALIDATION_CONSTRAINTS.QUALITY_RANGE.max}, got ${settings.quality}` }); } } if (settings.spacing < VALIDATION_CONSTRAINTS.SPACING_RANGE.min || settings.spacing > VALIDATION_CONSTRAINTS.SPACING_RANGE.max) { errors.push({ type: "processing", message: `Spacing must be between ${VALIDATION_CONSTRAINTS.SPACING_RANGE.min}px and ${VALIDATION_CONSTRAINTS.SPACING_RANGE.max}px, got ${settings.spacing}px` }); } if (!isValidHexColor(settings.backgroundColor)) { errors.push({ type: "processing", message: `Invalid background color format "${settings.backgroundColor}". Expected hex color like "#ffffff"` }); } if (settings.format === "pdf" && settings.pdfPageSize) { const validPageSizes = ["a4", "letter", "a3"]; if (!validPageSizes.includes(settings.pdfPageSize)) { errors.push({ type: "processing", message: `Invalid PDF page size "${settings.pdfPageSize}". Valid options: ${validPageSizes.join(", ")}` }); } } return errors; } function isValidHexColor(color) { const hexPattern = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; return hexPattern.test(color); } function formatFileSize(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } function validateEnvironment() { const errors = []; if (typeof window === "undefined") { errors.push({ type: "initialization", message: "This library requires a browser environment with DOM support" }); return errors; } if (!window.URL || !window.URL.createObjectURL) { errors.push({ type: "initialization", message: "URL.createObjectURL is not supported in this browser" }); } if (!window.FileReader) { errors.push({ type: "initialization", message: "FileReader is not supported in this browser" }); } if (!document.createElement) { errors.push({ type: "initialization", message: "DOM manipulation is not supported in this environment" }); } return errors; } function createMergeError(type, message, originalError, fileName) { return { type, message, originalError, fileName }; } var init_validation = __esm({ "src/utils/validation.ts"() { "use strict"; init_types(); } }); // src/utils/fileProcessing.ts function fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") { resolve(reader.result); } else { reject(createMergeError( "processing", "Failed to read file as data URL", void 0, file.name )); } }; reader.onerror = () => { reject(createMergeError( "processing", "FileReader error occurred", void 0, file.name )); }; reader.readAsDataURL(file); }); } function getImageDimensions(file) { return new Promise((resolve, reject) => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { URL.revokeObjectURL(url); resolve({ width: img.naturalWidth, height: img.naturalHeight }); }; img.onerror = () => { URL.revokeObjectURL(url); reject(createMergeError( "processing", "Failed to load image dimensions", void 0, file.name )); }; img.src = url; }); } async function processFiles(files) { const images = []; const errors = []; for (let i = 0; i < files.length; i++) { const file = files[i]; try { if (!VALIDATION_CONSTRAINTS.ALLOWED_TYPES.includes(file.type)) { errors.push(createMergeError( "file_type", `File "${file.name}" has unsupported type "${file.type}"`, void 0, file.name )); continue; } if (file.size > VALIDATION_CONSTRAINTS.MAX_FILE_SIZE) { errors.push(createMergeError( "file_size", `File "${file.name}" is too large (${formatBytes(file.size)})`, void 0, file.name )); continue; } const dimensions = await getImageDimensions(file); if (dimensions.width <= 0 || dimensions.height <= 0) { errors.push(createMergeError( "processing", `File "${file.name}" has invalid dimensions (${dimensions.width}x${dimensions.height})`, void 0, file.name )); continue; } const url = URL.createObjectURL(file); const imageFile = { id: `img-${Date.now()}-${i}`, file, url, name: file.name, size: file.size, type: file.type, width: dimensions.width, height: dimensions.height }; images.push(imageFile); } catch (error) { errors.push(createMergeError( "processing", `Failed to process file "${file.name}": ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0, file.name )); } } return { images, errors }; } function cleanupImageFiles(images) { images.forEach((image) => { if (image.url && image.url.startsWith("blob:")) { URL.revokeObjectURL(image.url); } }); } function validateImageFile(file) { return new Promise((resolve) => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { URL.revokeObjectURL(url); resolve(true); }; img.onerror = () => { URL.revokeObjectURL(url); resolve(false); }; img.src = url; }); } function formatBytes(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } function generateImageId() { return `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } function blobToFile(blob, filename) { return new File([blob], filename, { type: blob.type, lastModified: Date.now() }); } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => { URL.revokeObjectURL(url); }, 100); } var init_fileProcessing = __esm({ "src/utils/fileProcessing.ts"() { "use strict"; init_types(); init_validation(); } }); // src/utils/layout.ts function calculateLayout(images, settings) { if (images.length === 0) { return { canvasWidth: 0, canvasHeight: 0, positions: [] }; } const { direction, spacing } = settings; let canvasWidth = 0; let canvasHeight = 0; if (direction === "horizontal") { canvasWidth = images.reduce((sum, img) => sum + (img.width || 0), 0) + spacing * (images.length - 1); canvasHeight = Math.max(...images.map((img) => img.height || 0)); } else { canvasWidth = Math.max(...images.map((img) => img.width || 0)); canvasHeight = images.reduce((sum, img) => sum + (img.height || 0), 0) + spacing * (images.length - 1); } const positions = []; let currentX = 0; let currentY = 0; images.forEach((image) => { const imageWidth = image.width || 0; const imageHeight = image.height || 0; if (direction === "horizontal") { const top = Math.floor((canvasHeight - imageHeight) / 2); positions.push({ x: currentX, y: top, width: imageWidth, height: imageHeight }); currentX += imageWidth + spacing; } else { const left = Math.floor((canvasWidth - imageWidth) / 2); positions.push({ x: left, y: currentY, width: imageWidth, height: imageHeight }); currentY += imageHeight + spacing; } }); return { canvasWidth, canvasHeight, positions }; } function validateLayout(layout) { const warnings = []; const maxRecommendedSize = 1e4; if (layout.canvasWidth > maxRecommendedSize) { warnings.push(`Canvas width (${layout.canvasWidth}px) exceeds recommended maximum (${maxRecommendedSize}px)`); } if (layout.canvasHeight > maxRecommendedSize) { warnings.push(`Canvas height (${layout.canvasHeight}px) exceeds recommended maximum (${maxRecommendedSize}px)`); } const totalPixels = layout.canvasWidth * layout.canvasHeight; const maxRecommendedPixels = 100 * 1024 * 1024; if (totalPixels > maxRecommendedPixels) { warnings.push(`Total canvas size (${Math.round(totalPixels / (1024 * 1024))}M pixels) may cause memory issues`); } for (let i = 0; i < layout.positions.length; i++) { for (let j = i + 1; j < layout.positions.length; j++) { if (doPositionsOverlap(layout.positions[i], layout.positions[j])) { warnings.push(`Images ${i} and ${j} overlap in the calculated layout`); } } } return warnings; } function doPositionsOverlap(pos1, pos2) { return !(pos1.x + pos1.width <= pos2.x || pos2.x + pos2.width <= pos1.x || pos1.y + pos1.height <= pos2.y || pos2.y + pos2.height <= pos1.y); } function estimateMemoryUsage(layout, format) { const totalPixels = layout.canvasWidth * layout.canvasHeight; let bytesPerPixel; switch (format.toLowerCase()) { case "png": bytesPerPixel = 4; break; case "jpeg": case "jpg": bytesPerPixel = 3; break; default: bytesPerPixel = 4; } const estimatedBytes = totalPixels * bytesPerPixel; const megabytes = estimatedBytes / (1024 * 1024); let description; if (megabytes < 1) { description = "Small"; } else if (megabytes < 10) { description = "Medium"; } else if (megabytes < 50) { description = "Large"; } else { description = "Very Large"; } return { bytes: estimatedBytes, megabytes: Math.round(megabytes * 100) / 100, description }; } var init_layout = __esm({ "src/utils/layout.ts"() { "use strict"; } }); // src/core/TldrawMerger.ts var import_tldraw, import_client, import_react, TldrawMerger; var init_TldrawMerger = __esm({ "src/core/TldrawMerger.ts"() { "use strict"; import_tldraw = require("@tldraw/tldraw"); import_client = require("react-dom/client"); import_react = __toESM(require("react")); init_fileProcessing(); init_layout(); init_validation(); TldrawMerger = class { constructor(options = {}) { this.editor = null; this.container = null; this.root = null; this.isInitialized = false; this.options = { debug: false, maxCanvasSize: { width: 1e4, height: 1e4 }, ...options }; } /** * Initializes the TLDraw editor instance */ async initialize() { if (this.isInitialized) { return; } return new Promise((resolve, reject) => { try { this.container = this.options.container || document.createElement("div"); if (!this.options.container && this.container) { this.container.style.cssText = ` position: absolute; left: -9999px; top: -9999px; width: 800px; height: 600px; visibility: ${this.options.debug ? "visible" : "hidden"}; pointer-events: none; z-index: ${this.options.debug ? "1000" : "-1"}; border: ${this.options.debug ? "2px solid red" : "none"}; `; if (this.container) { document.body.appendChild(this.container); } } if (this.container) { this.root = (0, import_client.createRoot)(this.container); } const TldrawComponent = import_react.default.createElement(import_tldraw.Tldraw, { onMount: (editor) => { this.editor = editor; this.isInitialized = true; console.log("TldrawMerger initialized successfully"); resolve(); }, // Disable UI for programmatic use components: this.options.debug ? void 0 : { DebugPanel: null, DebugMenu: null, MainMenu: null, NavigationPanel: null, Toolbar: null, StylePanel: null, PageMenu: null, ActionsMenu: null, HelpMenu: null, ZoomMenu: null, QuickActions: null } }); this.root?.render(TldrawComponent); setTimeout(() => { if (!this.isInitialized) { reject(createMergeError( "initialization", "TLDraw editor failed to initialize within timeout period" )); } }, 1e4); } catch (error) { console.error("Failed to initialize TldrawMerger:", error); reject(createMergeError( "initialization", "Failed to initialize TLDraw editor", error instanceof Error ? error : void 0 )); } }); } /** * Merges images using TLDraw canvas rendering */ async merge(images, settings, onProgress) { if (!this.isInitialized || !this.editor) { throw createMergeError( "initialization", "TldrawMerger not initialized. Call initialize() first." ); } const imageErrors = validateImages(images); const settingsErrors = validateSettings(settings); const allErrors = [...imageErrors, ...settingsErrors]; if (allErrors.length > 0) { throw createMergeError( "processing", `Validation failed: ${allErrors.map((e) => e.message).join("; ")}` ); } try { onProgress?.(10); await this.clearCanvas(); onProgress?.(20); const layout = calculateLayout(images, settings); this.validateCanvasSize(layout); onProgress?.(30); const assets = await this.createImageAssets(images, onProgress); onProgress?.(60); await this.positionImages(assets, layout.positions); onProgress?.(80); const result = await this.exportImage(settings, layout); onProgress?.(100); return result; } catch (error) { console.error("TldrawMerger merge failed:", error); throw createMergeError( "processing", `Image merge failed: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ); } } /** * Clears all shapes from the canvas */ async clearCanvas() { if (!this.editor) return; const existingShapes = this.editor.getCurrentPageShapes(); if (existingShapes.length > 0) { this.editor.deleteShapes(existingShapes.map((shape) => shape.id)); } } /** * Validates that canvas size is within acceptable limits */ validateCanvasSize(layout) { const { maxCanvasSize } = this.options; if (!maxCanvasSize) return; if (layout.canvasWidth > maxCanvasSize.width || layout.canvasHeight > maxCanvasSize.height) { throw createMergeError( "processing", `Canvas size (${layout.canvasWidth}x${layout.canvasHeight}) exceeds maximum allowed size (${maxCanvasSize.width}x${maxCanvasSize.height})` ); } } /** * Creates TLDraw assets from image files */ async createImageAssets(images, onProgress) { if (!this.editor) throw createMergeError("initialization", "Editor not available"); const assets = []; for (const [index, image] of images.entries()) { try { const dataUrl = await fileToDataUrl(image.file); const assetId = `asset:${index}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; this.editor.createAssets([{ id: assetId, type: "image", typeName: "asset", props: { name: image.file.name, src: dataUrl, w: image.width || 0, h: image.height || 0, mimeType: image.file.type, isAnimated: false }, meta: {} }]); assets.push({ assetId, imageFile: image }); const progressIncrement = 30 / images.length; onProgress?.(30 + (index + 1) * progressIncrement); } catch (error) { throw createMergeError( "processing", `Failed to create asset for image "${image.name}": ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0, image.name ); } } return assets; } /** * Positions image shapes on the canvas */ async positionImages(assets, positions) { if (!this.editor) throw createMergeError("initialization", "Editor not available"); const shapesToCreate = assets.map(({ assetId, imageFile }, index) => { const position = positions[index]; return { type: "image", x: position.x, y: position.y, props: { assetId, w: position.width, h: position.height } }; }); this.editor.createShapes(shapesToCreate); } /** * Exports the canvas as an image */ async exportImage(settings, layout) { if (!this.editor) throw createMergeError("initialization", "Editor not available"); const allShapes = this.editor.getCurrentPageShapes(); if (allShapes.length === 0) { throw createMergeError("processing", "No shapes found on canvas for export"); } const exportResult = await this.editor.toImage(allShapes, { format: settings.format, quality: settings.format === "jpeg" ? settings.quality / 100 : 1, background: true }); if (!exportResult) { throw createMergeError("processing", "Failed to export image from TLDraw canvas"); } const result = { url: URL.createObjectURL(exportResult.blob), filename: `merged-image.${settings.format}`, size: exportResult.blob.size, format: settings.format }; return result; } /** * Cleanup resources */ destroy() { try { if (this.root) { this.root.unmount(); this.root = null; } if (this.container && !this.options.container && document.body.contains(this.container)) { document.body.removeChild(this.container); } this.container = null; this.editor = null; this.isInitialized = false; console.log("TldrawMerger destroyed successfully"); } catch (error) { console.error("Error destroying TldrawMerger:", error); } } /** * Gets the current initialization status */ get initialized() { return this.isInitialized; } /** * Gets the TLDraw editor instance (for advanced use cases) */ get editorInstance() { return this.editor; } }; } }); // src/core/PDFGenerator.ts var import_pdf_lib, PDF_PAGE_SIZES, PDFGenerator; var init_PDFGenerator = __esm({ "src/core/PDFGenerator.ts"() { "use strict"; import_pdf_lib = require("pdf-lib"); init_validation(); init_fileProcessing(); PDF_PAGE_SIZES = { a4: { width: 595, height: 842 }, // 210 × 297mm letter: { width: 612, height: 792 }, // 8.5 × 11 inches a3: { width: 842, height: 1191 } // 297 × 420mm }; PDFGenerator = class { /** * Generates a PDF document from an array of images * Each image becomes a separate page in the PDF */ static async generatePDF(images, settings, options = {}) { const { onProgress } = options; const imageErrors = validateImages(images); const settingsErrors = validateSettings(settings); const allErrors = [...imageErrors, ...settingsErrors]; if (allErrors.length > 0) { throw createMergeError( "processing", `PDF generation validation failed: ${allErrors.map((e) => e.message).join("; ")}` ); } if (settings.format !== "pdf") { throw createMergeError( "processing", 'PDF generation requires format to be set to "pdf"' ); } try { onProgress?.(5); const pdfDoc = await import_pdf_lib.PDFDocument.create(); const pageSize = PDF_PAGE_SIZES[settings.pdfPageSize || "a4"]; onProgress?.(10); for (let i = 0; i < images.length; i++) { const image = images[i]; try { const dataUrl = await fileToDataUrl(image.file); let pdfImage; if (image.type === "image/jpeg" || image.type === "image/jpg") { const imageBytes = await this.dataUrlToBytes(dataUrl); pdfImage = await pdfDoc.embedJpg(imageBytes); } else if (image.type === "image/png") { const imageBytes = await this.dataUrlToBytes(dataUrl); pdfImage = await pdfDoc.embedPng(imageBytes); } else { throw createMergeError( "processing", `Unsupported image type for PDF: ${image.type}`, void 0, image.name ); } const page = pdfDoc.addPage([pageSize.width, pageSize.height]); const imageDims = this.calculateImageDimensions( pdfImage.width, pdfImage.height, pageSize.width, pageSize.height ); const x = (pageSize.width - imageDims.width) / 2; const y = (pageSize.height - imageDims.height) / 2; page.drawImage(pdfImage, { x, y, width: imageDims.width, height: imageDims.height }); if (settings.backgroundColor && settings.backgroundColor !== "#ffffff") { const color = this.hexToRgb(settings.backgroundColor); page.drawRectangle({ x: 0, y: 0, width: pageSize.width, height: pageSize.height, color: (0, import_pdf_lib.rgb)(color.r / 255, color.g / 255, color.b / 255), opacity: 0.1 // Light background }); } const progressIncrement = 80 / images.length; onProgress?.(10 + (i + 1) * progressIncrement); } catch (error) { throw createMergeError( "processing", `Failed to process image "${image.name}" for PDF: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0, image.name ); } } onProgress?.(95); const pdfBytes = await pdfDoc.save(); onProgress?.(100); return pdfBytes; } catch (error) { console.error("PDF generation failed:", error); throw createMergeError( "processing", `PDF generation failed: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ); } } /** * Converts a data URL to byte array */ static async dataUrlToBytes(dataUrl) { const response = await fetch(dataUrl); const buffer = await response.arrayBuffer(); return new Uint8Array(buffer); } /** * Calculates image dimensions to fit within page bounds while maintaining aspect ratio */ static calculateImageDimensions(imageWidth, imageHeight, pageWidth, pageHeight, padding = 20) { const availableWidth = pageWidth - padding * 2; const availableHeight = pageHeight - padding * 2; const imageAspectRatio = imageWidth / imageHeight; const pageAspectRatio = availableWidth / availableHeight; let targetWidth; let targetHeight; if (imageAspectRatio > pageAspectRatio) { targetWidth = availableWidth; targetHeight = targetWidth / imageAspectRatio; } else { targetHeight = availableHeight; targetWidth = targetHeight * imageAspectRatio; } return { width: Math.max(1, Math.floor(targetWidth)), height: Math.max(1, Math.floor(targetHeight)) }; } /** * Converts hex color to RGB values */ static hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!result) { return { r: 255, g: 255, b: 255 }; } return { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) }; } /** * Creates a PDF result object from generated bytes */ static createPDFResult(pdfBytes) { const blob = new Blob([pdfBytes], { type: "application/pdf" }); return { url: URL.createObjectURL(blob), filename: "merged-images.pdf", size: blob.size, format: "pdf" }; } /** * Gets available PDF page sizes */ static getAvailablePageSizes() { return PDF_PAGE_SIZES; } /** * Validates if a page size is supported */ static isValidPageSize(pageSize) { return pageSize in PDF_PAGE_SIZES; } }; } }); // src/core/ImageMerger.ts var ImageMerger_exports = {}; __export(ImageMerger_exports, { ImageMerger: () => ImageMerger }); var ImageMerger; var init_ImageMerger = __esm({ "src/core/ImageMerger.ts"() { "use strict"; init_TldrawMerger(); init_PDFGenerator(); init_types(); init_validation(); init_fileProcessing(); init_layout(); ImageMerger = class { constructor(options = {}) { this.tldrawMerger = null; this.isInitialized = false; this.options = options; } /** * Initializes the image merger * Must be called before using any merge methods */ async initialize() { if (this.isInitialized) { return; } const envErrors = validateEnvironment(); if (envErrors.length > 0) { throw createMergeError( "initialization", `Environment validation failed: ${envErrors.map((e) => e.message).join("; ")}` ); } try { this.tldrawMerger = new TldrawMerger(this.options); await this.tldrawMerger.initialize(); this.isInitialized = true; console.log("ImageMerger initialized successfully"); } catch (error) { throw createMergeError( "initialization", `Failed to initialize ImageMerger: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ); } } /** * Merges an array of File objects * This is the main entry point for most use cases */ async mergeFiles(files, settings = {}, onProgress) { if (!this.isInitialized) { throw createMergeError( "initialization", "ImageMerger not initialized. Call initialize() first." ); } try { onProgress?.(5); const { images, errors } = await processFiles(files); if (errors.length > 0) { cleanupImageFiles(images); throw createMergeError( "processing", `File processing failed: ${errors.map((e) => e.message).join("; ")}` ); } onProgress?.(15); const result = await this.mergeImages(images, settings, (progress) => { onProgress?.(15 + progress * 0.85); }); cleanupImageFiles(images); return result; } catch (error) { throw createMergeError( "processing", `File merge failed: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ); } } /** * Merges an array of ImageFile objects * Use this if you have already processed files or need more control */ async mergeImages(images, settings = {}, onProgress) { if (!this.isInitialized) { throw createMergeError( "initialization", "ImageMerger not initialized. Call initialize() first." ); } const mergeSettings = { ...DEFAULT_MERGE_SETTINGS, ...settings }; const imageErrors = validateImages(images); const settingsErrors = validateSettings(mergeSettings); const allErrors = [...imageErrors, ...settingsErrors]; if (allErrors.length > 0) { throw createMergeError( "processing", `Validation failed: ${allErrors.map((e) => e.message).join("; ")}` ); } try { if (mergeSettings.format === "pdf") { return await this.generatePDF(images, mergeSettings, onProgress); } else { if (!this.tldrawMerger) { throw createMergeError("initialization", "TLDraw merger not available"); } return await this.tldrawMerger.merge(images, mergeSettings, onProgress); } } catch (error) { throw createMergeError( "processing", `Image merge failed: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ); } } /** * Generates a PDF from images (internal method) */ async generatePDF(images, settings, onProgress) { try { const pdfBytes = await PDFGenerator.generatePDF(images, settings, { onProgress }); return PDFGenerator.createPDFResult(pdfBytes); } catch (error) { throw createMergeError( "processing", `PDF generation failed: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 ); } } /** * Calculates the layout for given images and settings without merging * Useful for previewing the result or validating settings */ calculateLayout(images, settings = {}) { const mergeSettings = { ...DEFAULT_MERGE_SETTINGS, ...settings }; const layout = calculateLayout(images, mergeSettings); const warnings = validateLayout(layout); return { layout, warnings, settings: mergeSettings }; } /** * Validates files before processing * Returns validation errors without processing the files */ async validateFiles(files) { try { const { images, errors } = await processFiles(files); const warnings = []; if (images.length > 20) { warnings.push(`Processing ${images.length} images may take longer and use more memory`); } const totalSize = images.reduce((sum, img) => sum + img.size, 0); if (totalSize > 50 * 1024 * 1024) { warnings.push(`Total file size (${Math.round(totalSize / (1024 * 1024))}MB) is quite large`); } cleanupImageFiles(images); return { valid: errors.length === 0, errors, warnings }; } catch (error) { return { valid: false, errors: [createMergeError( "processing", `File validation failed: ${error instanceof Error ? error.message : "Unknown error"}`, error instanceof Error ? error : void 0 )], warnings: [] }; } } /** * Gets information about supported formats and constraints */ getCapabilities() { return { supportedInputFormats: ["image/jpeg", "image/jpg", "image/png"], supportedOutputFormats: ["jpeg", "png", "pdf"], maxFileSize: 100 * 1024 * 1024, // 100MB maxFileCount: 50, pdfPageSizes: Object.keys(PDFGenerator.getAvailablePageSizes()), qualityRange: { min: 10, max: 100 }, spacingRange: { min: 0, max: 200 } }; } /** * Cleanup all resources * Should be called when done using the merger to prevent memory leaks */ destroy() { try { if (this.tldrawMerger) { this.tldrawMerger.destroy(); this.tldrawMerger = null; } this.isInitialized = false; console.log("ImageMerger destroyed successfully"); } catch (error) { console.error("Error destroying ImageMerger:", error); } } /** * Gets current initialization status */ get initialized() { return this.isInitialized; } /** * Gets the underlying TLDraw merger instance for advanced usage */ get tldrawInstance() { return this.tldrawMerger; } }; } }); // src/index.ts var index_exports = {}; __export(index_exports, { DEFAULT_MERGE_SETTINGS: () => DEFAULT_MERGE_SETTINGS, ImageMerger: () => ImageMerger, PDFGenerator: () => PDFGenerator, TldrawMerger: () => TldrawMerger, VALIDATION_CONSTRAINTS: () => VALIDATION_CONSTRAINTS, blobToFile: () => blobToFile, calculateLayout: () => calculateLayout, cleanupImageFiles: () => cleanupImageFiles, createMergeError: () => createMergeError, downloadBlob: () => downloadBlob, estimateMemoryUsage: () => estimateMemoryUsage, fileToDataUrl: () => fileToDataUrl, formatFileSize: () => formatFileSize, generateImageId: () => generateImageId, getCapabilities: () => getCapabilities, getImageDimensions: () => getImageDimensions, isValidHexColor: () => isValidHexColor, mergeFiles: () => mergeFiles, mergeImages: () => mergeImages, processFiles: () => processFiles, validateEnvironment: () => validateEnvironment, validateFiles: () => validateFiles, validateImageFile: () => validateImageFile, validateImages: () => validateImages, validateLayout: () => validateLayout, validateSettings: () => validateSettings, version: () => version }); module.exports = __toCommonJS(index_exports); init_ImageMerger(); init_TldrawMerger(); init_PDFGenerator(); init_types(); init_validation(); init_layout(); init_fileProcessing(); async function mergeFiles(files, settings, onProgress) { const merger = new (await Promise.resolve().then(() => (init_ImageMerger(), ImageMerger_exports))).ImageMerger(); try { await merger.initialize(); return await merger.mergeFiles(files, settings, onProgress); } finally { merger.destroy(); } } async function mergeImages(images, settings, onProgress) { const merger = new (await Promise.resolve().then(() => (init_ImageMerger(), ImageMerger_exports))).ImageMerger(); try { await merger.initialize(); return await merger.mergeImages(images, settings, onProgress); } finally { merger.destroy(); } } async function validateFiles(files) { const merger = new (await Promise.resolve().then(() => (init_ImageMerger(), ImageMerger_exports))).ImageMerger(); try { await merger.initialize(); return await merger.validateFiles(files); } finally { merger.destroy(); } } function getCapabilities() { return { supportedInputFormats: ["image/jpeg", "image/jpg", "image/png"], supportedOutputFormats: ["jpeg", "png", "pdf"], maxFileSize: 100 * 1024 * 1024, // 100MB maxFileCount: 50, pdfPageSizes: ["a4", "letter", "a3"], qualityRange: { min: 10, max: 100 }, spacingRange: { min: 0, max: 200 } }; } var version = "1.0.0"; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { DEFAULT_MERGE_SETTINGS, ImageMerger, PDFGenerator, TldrawMerger, VALIDATION_CONSTRAINTS, blobToFile, calculateLayout, cleanupImageFiles, createMergeError, downloadBlob, estimateMemoryUsage, fileToDataUrl, formatFileSize, generateImageId, getCapabilities, getImageDimensions, isValidHexColor, mergeFiles, mergeImages, processFiles, validateEnvironment, validateFiles, validateImageFile, validateImages, validateLayout, validateSettings, version });