merge-jpg
Version:
A privacy-first client-side image merging library powered by TLDraw Canvas
1,286 lines (1,277 loc) • 42.1 kB
JavaScript
;
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
});