document.addEventListener('DOMContentLoaded', function () { const { PDFDocument } = PDFLib; const dropArea = document.getElementById('drop-area'); const fileInput = document.getElementById('file-input'); const fileSelectBtn = document.getElementById('file-select-btn'); const pdfInfo = document.getElementById('pdf-info'); const pdfFilename = document.getElementById('pdf-filename'); const pdfPageCount = document.getElementById('pdf-page-count'); const pdfPaperCount = document.getElementById('pdf-paper-count'); const previewBtn = document.getElementById('preview-btn'); const exportSingleBtn = document.getElementById('export-single-btn'); const exportZipBtn = document.getElementById('export-zip-btn'); const bookletSizeInput = document.getElementById('booklet-size'); const firstBookletSizeInput = document.getElementById('first-booklet-size'); const blankPagesInput = document.getElementById('blank-pages-start'); let originalPdfBytes = null; let originalPageCount = 0; // Event Listeners ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropArea.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false); }); ['dragenter', 'dragover'].forEach(eventName => dropArea.addEventListener(eventName, () => dropArea.classList.add('drag-over'), false)); ['dragleave', 'drop'].forEach(eventName => dropArea.addEventListener(eventName, () => dropArea.classList.remove('drag-over'), false)); dropArea.addEventListener('drop', handleDrop, false); fileSelectBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleFileSelect, false); previewBtn.addEventListener('click', updatePreview); exportSingleBtn.addEventListener('click', exportAsSinglePdf); exportZipBtn.addEventListener('click', exportAsZip); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } function handleDrop(e) { handleFiles(e.dataTransfer.files); } function handleFileSelect(e) { handleFiles(e.target.files); } async function handleFiles(files) { if (files.length > 0) { const file = files[0]; if (file.type === 'application/pdf') { pdfFilename.textContent = file.name; dropArea.classList.add('loaded'); dropArea.querySelector('p').textContent = 'Drag & drop or click to select a different PDF.'; const fileReader = new FileReader(); fileReader.onload = async function() { originalPdfBytes = new Uint8Array(this.result); try { const pdfDoc = await PDFDocument.load(originalPdfBytes); originalPageCount = pdfDoc.getPageCount(); pdfPageCount.textContent = originalPageCount; const paperCount = Math.ceil(originalPageCount / 4); pdfPaperCount.textContent = paperCount; pdfInfo.classList.remove('d-none'); document.getElementById('recommendation-section').classList.remove('d-none'); previewBtn.disabled = false; exportSingleBtn.disabled = false; exportZipBtn.disabled = false; updateRecommendationTable(); updatePreview(); // Auto-update preview on new file } catch (e) { console.error(e); alert('Failed to load PDF. The file may be corrupt.'); } }; fileReader.readAsArrayBuffer(file); } else { alert('Please select a PDF file.'); } } } function padAndImposeBooklet(bookletPages) { const paddedBookletSize = Math.ceil(bookletPages.length / 4) * 4; while (bookletPages.length < paddedBookletSize) { bookletPages.push({ type: 'blank' }); } return imposeBooklet(bookletPages); } function calculateImposition() { const bookletSize = parseInt(bookletSizeInput.value, 10); let firstBookletSize = parseInt(firstBookletSizeInput.value, 10) || bookletSize; const blankPagesAtStart = parseInt(blankPagesInput.value, 10) || 0; if (bookletSize % 4 !== 0 || (firstBookletSize && firstBookletSize % 4 !== 0)) { alert('Booklet sizes must be a multiple of 4.'); return null; } const totalPages = originalPageCount + blankPagesAtStart; let sourcePages = []; for (let i = 0; i < blankPagesAtStart; i++) sourcePages.push({ type: 'blank' }); for (let i = 1; i <= originalPageCount; i++) sourcePages.push({ type: 'pdf', pageNum: i }); let booklets = []; // First booklet let firstBookletActualSize = (firstBookletSize > 0 && totalPages > 0) ? firstBookletSize : bookletSize; if (totalPages < firstBookletSize) { firstBookletActualSize = Math.ceil(totalPages / 4) * 4; } const firstBookletPages = sourcePages.splice(0, firstBookletActualSize); booklets.push(padAndImposeBooklet(firstBookletPages)); // Remaining booklets while (sourcePages.length > 0) { const bookletPages = sourcePages.splice(0, bookletSize); booklets.push(padAndImposeBooklet(bookletPages)); } return booklets; } function imposeBooklet(bookletPages) { const size = bookletPages.length; const imposed = []; for (let i = 0; i < size / 2; i += 2) { imposed.push(bookletPages[size - 1 - i]); imposed.push(bookletPages[i]); imposed.push(bookletPages[i + 1]); imposed.push(bookletPages[size - 2 - i]); } return imposed; } function updatePreview() { const booklets = calculateImposition(); if (!booklets) return; const previewArea = document.getElementById('imposition-preview'); let html = ''; booklets.forEach((booklet, index) => { html += `
Booklet ${index + 1}
`; for (let i = 0; i < booklet.length; i += 4) { const sheetPages = booklet.slice(i, i + 4); html += `
Front
${sheetPages[0].type === 'blank' ? 'BLANK' : 'Page ' + sheetPages[0].pageNum}
${sheetPages[1].type === 'blank' ? 'BLANK' : 'Page ' + sheetPages[1].pageNum}
Back
${sheetPages[2].type === 'blank' ? 'BLANK' : 'Page ' + sheetPages[2].pageNum}
${sheetPages[3].type === 'blank' ? 'BLANK' : 'Page ' + sheetPages[3].pageNum}
`; } }); previewArea.innerHTML = html; } async function createBookletPdf(booklet, srcDoc) { const { width, height } = srcDoc.getPage(0).getSize(); const newDoc = await PDFDocument.create(); for (const pageInfo of booklet) { if (pageInfo.type === 'pdf') { const [copiedPage] = await newDoc.copyPages(srcDoc, [pageInfo.pageNum - 1]); newDoc.addPage(copiedPage); } else { newDoc.addPage([width, height]); } } return newDoc; } async function exportAsSinglePdf() { if (!originalPdfBytes) { alert('Please upload a PDF first.'); return; } exportSingleBtn.disabled = true; exportSingleBtn.innerHTML = ' Exporting...'; const booklets = calculateImposition(); if (!booklets) { exportSingleBtn.disabled = false; exportSingleBtn.textContent = 'Export as Single PDF'; return; } try { const srcDoc = await PDFDocument.load(originalPdfBytes); const mergedPdf = await PDFDocument.create(); for (const booklet of booklets) { const bookletPdf = await createBookletPdf(booklet, srcDoc); const copiedPages = await mergedPdf.copyPages(bookletPdf, bookletPdf.getPageIndices()); copiedPages.forEach((page) => mergedPdf.addPage(page)); } const pdfBytes = await mergedPdf.save(); download(pdfBytes, `imposed-booklet.pdf`, "application/pdf"); } catch (e) { console.error(e); alert('An error occurred while exporting the PDF.'); } finally { exportSingleBtn.disabled = false; exportSingleBtn.textContent = 'Export as Single PDF'; } } async function exportAsZip() { if (!originalPdfBytes) { alert('Please upload a PDF first.'); return; } exportZipBtn.disabled = true; exportZipBtn.innerHTML = ' Exporting...'; const booklets = calculateImposition(); if (!booklets) { exportZipBtn.disabled = false; exportZipBtn.textContent = 'Export as ZIP'; return; } try { const srcDoc = await PDFDocument.load(originalPdfBytes); const zip = new JSZip(); for (let i = 0; i < booklets.length; i++) { const booklet = booklets[i]; const bookletPdf = await createBookletPdf(booklet, srcDoc); const pdfBytes = await bookletPdf.save(); zip.file(`booklet-${i + 1}.pdf`, pdfBytes); } const zipBytes = await zip.generateAsync({ type: "blob" }); download(zipBytes, 'booklets.zip', 'application/zip'); } catch (e) { console.error(e); alert('An error occurred while exporting the ZIP file.'); } finally { exportZipBtn.disabled = false; exportZipBtn.textContent = 'Export as ZIP'; } } function download(data, filename, type) { const blob = new Blob([data], { type: type }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } function updateRecommendationTable() { if (originalPageCount === 0) return; const tableBody = document.getElementById('recommendation-table-body'); tableBody.innerHTML = ''; // Clear existing rows const totalPages = originalPageCount + (parseInt(blankPagesInput.value, 10) || 0); for (let size = 4; size <= 32; size += 4) { const numBooklets = Math.ceil(totalPages / size); const lastBookletSize = totalPages % size || size; const row = ` ${size} ${numBooklets} ${lastBookletSize} `; tableBody.innerHTML += row; if (numBooklets === 1) { break; // Stop after the first size that fits all pages } } } });