301 lines
12 KiB
JavaScript
301 lines
12 KiB
JavaScript
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 += `<h5 class="mt-3">Booklet ${index + 1}</h5>`;
|
|
for (let i = 0; i < booklet.length; i += 4) {
|
|
const sheetPages = booklet.slice(i, i + 4);
|
|
html += `
|
|
<div class="sheet-preview">
|
|
<div class="sheet-side">
|
|
<strong>Front</strong>
|
|
<div class="page-preview ${sheetPages[0].type === 'blank' ? 'blank-page' : ''}">${sheetPages[0].type === 'blank' ? 'BLANK' : 'Page ' + sheetPages[0].pageNum}</div>
|
|
<div class="page-preview ${sheetPages[1].type === 'blank' ? 'blank-page' : ''}">${sheetPages[1].type === 'blank' ? 'BLANK' : 'Page ' + sheetPages[1].pageNum}</div>
|
|
</div>
|
|
<div class="sheet-side">
|
|
<strong>Back</strong>
|
|
<div class="page-preview ${sheetPages[2].type === 'blank' ? 'blank-page' : ''}">${sheetPages[2].type === 'blank' ? 'BLANK' : 'Page ' + sheetPages[2].pageNum}</div>
|
|
<div class="page-preview ${sheetPages[3].type === 'blank' ? 'blank-page' : ''}">${sheetPages[3].type === 'blank' ? 'BLANK' : 'Page ' + sheetPages[3].pageNum}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
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 = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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 = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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 = `
|
|
<tr>
|
|
<td>${size}</td>
|
|
<td>${numBooklets}</td>
|
|
<td>${lastBookletSize}</td>
|
|
</tr>
|
|
`;
|
|
tableBody.innerHTML += row;
|
|
|
|
if (numBooklets === 1) {
|
|
break; // Stop after the first size that fits all pages
|
|
}
|
|
}
|
|
}
|
|
}); |