PDF Merging

CustomJS provides robust PDF merging capabilities that allow you to combine multiple PDF documents into a single file within your Make.com workflows. This is essential for document consolidation, report compilation, and automated document processing.

Overview

The PDF merging module can combine PDFs from various sources including:

  • File uploads from previous modules
  • Base64 encoded PDF data
  • URLs pointing to PDF documents
  • Generated PDFs from other CustomJS modules

Key Features

  • Multiple input formats (binary, base64, URLs)
  • Page range selection from source PDFs
  • Custom page ordering and arrangement
  • Metadata preservation and customization
  • Bookmarks and navigation handling
  • Password-protected PDF support

Basic PDF Merging

Simple Two-PDF Merge

// Basic merge of two PDF documents
const mergeConfig = {
    pdfs: [
        {
            source: input.pdf1, // Base64 or binary data
            name: 'document1.pdf',
            pages: 'all' // or specific range like '1-5'
        },
        {
            source: input.pdf2,
            name: 'document2.pdf', 
            pages: 'all'
        }
    ],
    outputFilename: `merged-${Date.now()}.pdf`,
    preserveBookmarks: true,
    addPageNumbers: false
};

return {
    mergeConfig,
    success: true,
    timestamp: new Date().toISOString()
};

Multiple PDF Merge with Ordering

// Merge multiple PDFs with custom ordering
const documents = input.documents || [];

const mergeConfig = {
    pdfs: documents.map((doc, index) => ({
        source: doc.content, // PDF content (base64 or binary)
        name: doc.filename || `document-${index + 1}.pdf`,
        pages: doc.pageRange || 'all',
        order: doc.order || index,
        metadata: {
            title: doc.title,
            author: doc.author,
            subject: doc.subject
        }
    })).sort((a, b) => a.order - b.order), // Sort by order
    
    outputFilename: input.outputName || `merged-documents-${new Date().toISOString().split('T')[0]}.pdf`,
    
    // Merge options
    preserveBookmarks: input.preserveBookmarks !== false,
    addPageNumbers: input.addPageNumbers || false,
    addTableOfContents: input.addTableOfContents || false,
    
    // Output metadata
    metadata: {
        title: input.mergedTitle || 'Merged Document',
        author: input.author || 'CustomJS Automation',
        subject: input.subject || 'Automatically merged PDF',
        creator: 'CustomJS PDF Merger',
        creationDate: new Date().toISOString()
    }
};

return mergeConfig;

Advanced Merging Options

Page Range Selection

// Merge specific pages from multiple documents
function createAdvancedMergeConfig(input) {
    const { sources, ranges, options = {} } = input;
    
    const pdfs = sources.map((source, index) => {
        const range = ranges[index] || 'all';
        
        return {
            source: source.content,
            name: source.filename,
            pages: parsePageRange(range),
            insertBlankPageAfter: source.insertBlankPageAfter || false,
            rotate: source.rotation || 0, // 0, 90, 180, 270
            scale: source.scale || 1.0
        };
    });
    
    return {
        pdfs,
        outputFilename: options.filename || 'merged.pdf',
        preserveBookmarks: options.preserveBookmarks !== false,
        addPageNumbers: options.addPageNumbers || false,
        pageNumberFormat: options.pageNumberFormat || 'Page {current} of {total}',
        pageNumberPosition: options.pageNumberPosition || 'bottom-center'
    };
}

function parsePageRange(range) {
    if (range === 'all') return 'all';
    
    // Handle ranges like "1-5", "1,3,5-7", "2-"
    const parts = range.split(',').map(part => part.trim());
    const parsedRanges = [];
    
    for (const part of parts) {
        if (part.includes('-')) {
            const [start, end] = part.split('-').map(p => p.trim());
            parsedRanges.push({
                start: parseInt(start) || 1,
                end: end ? parseInt(end) : 'end'
            });
        } else {
            const pageNum = parseInt(part);
            if (pageNum) {
                parsedRanges.push({ start: pageNum, end: pageNum });
            }
        }
    }
    
    return parsedRanges;
}

// Usage
const mergeConfig = createAdvancedMergeConfig(input);
return mergeConfig;

Conditional Merging

// Merge PDFs based on conditions and rules
function conditionalMerge(input) {
    const { documents, rules, options = {} } = input;
    
    // Filter documents based on rules
    const eligibleDocs = documents.filter(doc => {
        // Check file size limits
        if (rules.maxFileSize && doc.size > rules.maxFileSize) {
            return false;
        }
        
        // Check document type/category
        if (rules.allowedCategories && !rules.allowedCategories.includes(doc.category)) {
            return false;
        }
        
        // Check date range
        if (rules.dateRange) {
            const docDate = new Date(doc.createdAt);
            const startDate = new Date(rules.dateRange.start);
            const endDate = new Date(rules.dateRange.end);
            
            if (docDate < startDate || docDate > endDate) {
                return false;
            }
        }
        
        return true;
    });
    
    // Sort documents based on criteria
    const sortedDocs = eligibleDocs.sort((a, b) => {
        switch (rules.sortBy) {
            case 'date':
                return new Date(a.createdAt) - new Date(b.createdAt);
            case 'name':
                return a.filename.localeCompare(b.filename);
            case 'size':
                return a.size - b.size;
            case 'priority':
                return (b.priority || 0) - (a.priority || 0);
            default:
                return 0;
        }
    });
    
    // Limit number of documents if specified
    const finalDocs = rules.maxDocuments ? 
        sortedDocs.slice(0, rules.maxDocuments) : 
        sortedDocs;
    
    if (finalDocs.length === 0) {
        return {
            error: 'No documents meet the merge criteria',
            criteria: rules,
            totalDocuments: documents.length
        };
    }
    
    const mergeConfig = {
        pdfs: finalDocs.map((doc, index) => ({
            source: doc.content,
            name: doc.filename,
            pages: doc.pageRange || 'all',
            metadata: {
                originalIndex: documents.indexOf(doc),
                category: doc.category,
                priority: doc.priority
            }
        })),
        
        outputFilename: options.filename || `filtered-merge-${Date.now()}.pdf`,
        
        // Add summary page if requested
        addSummaryPage: options.addSummaryPage || false,
        summaryContent: options.addSummaryPage ? generateSummaryPage(finalDocs, rules) : null,
        
        metadata: {
            title: `Merged Document - ${finalDocs.length} files`,
            subject: `Merged based on criteria: ${JSON.stringify(rules)}`,
            keywords: finalDocs.map(doc => doc.category).filter(Boolean).join(', ')
        }
    };
    
    return {
        mergeConfig,
        summary: {
            totalInput: documents.length,
            filtered: finalDocs.length,
            excluded: documents.length - finalDocs.length,
            criteria: rules
        }
    };
}

function generateSummaryPage(documents, rules) {
    return `
    <div style="font-family: Arial, sans-serif; padding: 40px;">
        <h1>Document Merge Summary</h1>
        <p><strong>Generated:</strong> ${new Date().toLocaleString()}</p>
        <p><strong>Total Documents:</strong> ${documents.length}</p>
        
        <h2>Merge Criteria</h2>
        <ul>
            ${rules.maxFileSize ? `<li>Max file size: ${rules.maxFileSize} bytes</li>` : ''}
            ${rules.allowedCategories ? `<li>Categories: ${rules.allowedCategories.join(', ')}</li>` : ''}
            ${rules.dateRange ? `<li>Date range: ${rules.dateRange.start} to ${rules.dateRange.end}</li>` : ''}
            ${rules.sortBy ? `<li>Sorted by: ${rules.sortBy}</li>` : ''}
        </ul>
        
        <h2>Included Documents</h2>
        <table style="width: 100%; border-collapse: collapse;">
            <thead>
                <tr style="background: #f0f0f0;">
                    <th style="border: 1px solid #ccc; padding: 8px;">Filename</th>
                    <th style="border: 1px solid #ccc; padding: 8px;">Category</th>
                    <th style="border: 1px solid #ccc; padding: 8px;">Size</th>
                    <th style="border: 1px solid #ccc; padding: 8px;">Date</th>
                </tr>
            </thead>
            <tbody>
                ${documents.map(doc => `
                    <tr>
                        <td style="border: 1px solid #ccc; padding: 8px;">${doc.filename}</td>
                        <td style="border: 1px solid #ccc; padding: 8px;">${doc.category || 'N/A'}</td>
                        <td style="border: 1px solid #ccc; padding: 8px;">${formatFileSize(doc.size)}</td>
                        <td style="border: 1px solid #ccc; padding: 8px;">${new Date(doc.createdAt).toLocaleDateString()}</td>
                    </tr>
                `).join('')}
            </tbody>
        </table>
    </div>
    `;
}

function formatFileSize(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

Error Handling and Validation

Input Validation

// Comprehensive validation for PDF merge inputs
function validateMergeInputs(input) {
    const errors = [];
    const warnings = [];
    
    // Check if PDFs array exists and is valid
    if (!input.pdfs || !Array.isArray(input.pdfs)) {
        errors.push('PDFs array is required and must be an array');
        return { errors, warnings };
    }
    
    if (input.pdfs.length === 0) {
        errors.push('At least one PDF is required for merging');
        return { errors, warnings };
    }
    
    if (input.pdfs.length === 1) {
        warnings.push('Only one PDF provided - merge will create a copy');
    }
    
    // Validate each PDF
    input.pdfs.forEach((pdf, index) => {
        const pdfErrors = [];
        
        // Check source
        if (!pdf.source) {
            pdfErrors.push(`PDF ${index + 1}: source is required`);
        }
        
        // Validate page ranges
        if (pdf.pages && pdf.pages !== 'all') {
            if (!isValidPageRange(pdf.pages)) {
                pdfErrors.push(`PDF ${index + 1}: invalid page range format`);
            }
        }
        
        // Check rotation values
        if (pdf.rotate && ![0, 90, 180, 270].includes(pdf.rotate)) {
            pdfErrors.push(`PDF ${index + 1}: rotation must be 0, 90, 180, or 270 degrees`);
        }
        
        // Check scale values
        if (pdf.scale && (pdf.scale <= 0 || pdf.scale > 5)) {
            pdfErrors.push(`PDF ${index + 1}: scale must be between 0 and 5`);
        }
        
        errors.push(...pdfErrors);
    });
    
    // Validate output filename
    if (input.outputFilename && !/^[^<>:"/\\|?*]+\.pdf$/i.test(input.outputFilename)) {
        errors.push('Invalid output filename format');
    }
    
    // Check for potential memory issues
    if (input.pdfs.length > 50) {
        warnings.push('Merging more than 50 PDFs may cause performance issues');
    }
    
    return { errors, warnings };
}

function isValidPageRange(range) {
    // Validate page range formats like "1-5", "1,3,5-7", "2-"
    const rangePattern = /^(\d+(-\d*)?)(,\s*\d+(-\d*)?)*$/;
    return rangePattern.test(range.replace(/\s/g, ''));
}

// Main merge function with validation
function performMergeWithValidation(input) {
    const validation = validateMergeInputs(input);
    
    if (validation.errors.length > 0) {
        return {
            success: false,
            errors: validation.errors,
            warnings: validation.warnings
        };
    }
    
    try {
        const mergeConfig = buildMergeConfig(input);
        
        return {
            success: true,
            mergeConfig,
            warnings: validation.warnings,
            summary: {
                totalPDFs: input.pdfs.length,
                outputFilename: mergeConfig.outputFilename,
                estimatedPages: estimatePageCount(input.pdfs)
            }
        };
        
    } catch (error) {
        return {
            success: false,
            error: 'Failed to create merge configuration',
            details: error.message,
            timestamp: new Date().toISOString()
        };
    }
}

function estimatePageCount(pdfs) {
    // Rough estimation - in real implementation, this would analyze actual PDFs
    return pdfs.reduce((total, pdf) => {
        if (pdf.pages === 'all') {
            return total + 10; // Assume 10 pages if not specified
        } else if (typeof pdf.pages === 'string') {
            // Parse page ranges to estimate count
            const matches = pdf.pages.match(/\d+/g);
            return total + (matches ? matches.length * 2 : 5);
        }
        return total + 5;
    }, 0);
}

Merge Optimization Strategies

Memory-Efficient Merging

// Optimize merge for large files and many documents
function optimizedMerge(input) {
    const { pdfs, options = {} } = input;
    
    // Sort PDFs by size (merge smaller ones first)
    const sortedPdfs = [...pdfs].sort((a, b) => (a.estimatedSize || 0) - (b.estimatedSize || 0));
    
    // Batch processing for large numbers of PDFs
    const batchSize = options.batchSize || 10;
    const batches = [];
    
    for (let i = 0; i < sortedPdfs.length; i += batchSize) {
        batches.push(sortedPdfs.slice(i, i + batchSize));
    }
    
    const mergeConfig = {
        processingMode: 'batched',
        batches: batches.map((batch, index) => ({
            batchId: index,
            pdfs: batch,
            tempFilename: `batch-${index}-temp.pdf`
        })),
        
        finalMerge: {
            sources: batches.map((_, index) => `batch-${index}-temp.pdf`),
            outputFilename: options.outputFilename || 'final-merged.pdf',
            cleanupTempFiles: true
        },
        
        optimization: {
            compressImages: options.compressImages !== false,
            removeUnusedObjects: options.removeUnusedObjects !== false,
            optimizeForWeb: options.optimizeForWeb || false,
            maxMemoryUsage: options.maxMemoryUsage || '512MB'
        }
    };
    
    return mergeConfig;
}

Quality and Compression Options

// Configure merge with quality and compression settings
function configureMergeQuality(input) {
    const { pdfs, quality = 'balanced' } = input;
    
    const qualityPresets = {
        maximum: {
            imageCompression: false,
            imageQuality: 100,
            preserveAnnotations: true,
            preserveFormFields: true,
            preserveBookmarks: true,
            optimizeForPrint: true
        },
        balanced: {
            imageCompression: true,
            imageQuality: 85,
            preserveAnnotations: true,
            preserveFormFields: true,
            preserveBookmarks: true,
            optimizeForPrint: false
        },
        compressed: {
            imageCompression: true,
            imageQuality: 70,
            preserveAnnotations: false,
            preserveFormFields: false,
            preserveBookmarks: true,
            optimizeForPrint: false,
            removeMetadata: true
        }
    };
    
    const settings = qualityPresets[quality] || qualityPresets.balanced;
    
    return {
        pdfs,
        outputSettings: {
            ...settings,
            outputFilename: input.outputFilename || `merged-${quality}.pdf`
        },
        metadata: {
            compressionLevel: quality,
            processedAt: new Date().toISOString(),
            originalFileCount: pdfs.length
        }
    };
}

Common Merge Patterns

Document Assembly Workflow

// Assemble documents in a specific order for business workflows
function assembleBusinessDocument(input) {
    const { coverPage, mainContent, appendices, signatures } = input;
    
    const documentStructure = [
        // Cover page
        coverPage && {
            source: coverPage.content,
            name: 'cover.pdf',
            pages: 'all',
            addPageBreak: true
        },
        
        // Table of contents (if provided)
        input.tableOfContents && {
            source: input.tableOfContents.content,
            name: 'toc.pdf',
            pages: 'all',
            addPageBreak: true
        },
        
        // Main content sections
        ...mainContent.map((section, index) => ({
            source: section.content,
            name: `section-${index + 1}.pdf`,
            pages: section.pageRange || 'all',
            addPageBreak: index < mainContent.length - 1,
            bookmarkTitle: section.title
        })),
        
        // Appendices
        ...appendices.map((appendix, index) => ({
            source: appendix.content,
            name: `appendix-${String.fromCharCode(65 + index)}.pdf`,
            pages: 'all',
            addPageBreak: true,
            bookmarkTitle: `Appendix ${String.fromCharCode(65 + index)}: ${appendix.title}`
        })),
        
        // Signature pages
        signatures && {
            source: signatures.content,
            name: 'signatures.pdf',
            pages: 'all'
        }
    ].filter(Boolean);
    
    return {
        pdfs: documentStructure,
        outputFilename: `${input.documentTitle || 'business-document'}-${Date.now()}.pdf`,
        addBookmarks: true,
        addPageNumbers: true,
        pageNumberStartsAt: coverPage ? 2 : 1, // Skip cover page for numbering
        metadata: {
            title: input.documentTitle,
            author: input.author,
            subject: input.subject,
            keywords: input.keywords
        }
    };
}

Best Practices for PDF Merging

  1. Validate inputs before processing to avoid errors
  2. Handle large files with batching and memory management
  3. Preserve important metadata like bookmarks and form fields
  4. Use consistent naming for output files
  5. Implement proper error handling for failed merges
  6. Consider file size limits in your workflow
  7. Test with various PDF types (scanned, generated, forms)
  8. Optimize for your use case (web viewing vs. printing)
  9. Clean up temporary files after processing
  10. Log merge operations for debugging and auditing

PDF merging with CustomJS enables powerful document automation workflows, from simple file combination to complex document assembly processes in your Make.com scenarios.