Project avaliable on GitHub.

Overview
Libritos is a JavaScript-based tool designed to generate book-sized PDFs from full-sized PDFs. The generated PDFs can be cut in half and bound to create small books. This project leverages the pdf-lib and downloadjs libraries to achieve its functionality.
Features
- Convert full-sized PDFs to book-sized PDFs.
- Generate booklet PDFs.
- Arrange four pages on a single PDF page.
- Rearrange PDF pages for book printing.
Script
First we need to load the PDF file and the PNG image. We will use the fetch function to load the PDF file and the arrayBuffer method to convert the response to an array buffer.
const loadPdf = async (url) => {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return await PDFLib.PDFDocument.load(arrayBuffer);
};
We will also create a function to load the PNG image.
const loadPngImage = async (url) => {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer;
};
Event listeners are added to the dropzone label to handle the drag and drop events.
document.addEventListener('DOMContentLoaded', () => {
const dropzoneLabel = document.getElementById('dropzone-label');
const dropzoneFileInput = document.getElementById('dropzone-file');
dropzoneLabel.addEventListener('dragover', (event) => {
event.preventDefault();
dropzoneLabel.classList.add('bg-gray-300', 'border-blue-500');
});
dropzoneLabel.addEventListener('dragleave', () => {
dropzoneLabel.classList.remove('bg-gray-300', 'border-blue-500');
});
dropzoneLabel.addEventListener('drop', (event) => {
event.preventDefault();
dropzoneLabel.classList.remove('bg-gray-300', 'border-blue-500');
const files = event.dataTransfer.files;
if (files.length > 0) {
dropzoneFileInput.files = files;
// Trigger change event to handle the file upload
const changeEvent = new Event('change');
dropzoneFileInput.dispatchEvent(changeEvent);
}
});
});
Load the PDF file and extract the number of pages.
document.getElementById('dropzone-file').addEventListener('change', async function(event) {
try {
const fileInput = event.target;
const file = fileInput.files[0];
if (file) {
console.log('File selected:', file.name);
const arrayBuffer = await file.arrayBuffer();
console.log('Array buffer loaded');
const pdfDoc = await PDFLib.PDFDocument.load(arrayBuffer);
console.log('PDF loaded');
const numPages = pdfDoc.getPageCount();
We will also calculate the residual when the number of pages is divided by 4.
const residual = numPages % 4;
console.log(`Number of pages: ${numPages}`);
console.log(`Residual when divided by 4: ${residual}`);
We will then extract the first, second, penultimate, and last pages from the PDF file.
const firstPage = pdfDoc.getPage(0);
const secondPage = pdfDoc.getPage(1);
const penultimatePage = pdfDoc.getPage(numPages - 2);
const lastPage = pdfDoc.getPage(numPages - 1);
const { width, height } = firstPage.getSize();
A new PDF document is created with the pdf-lib library.
const newPdfDoc = await PDFLib.PDFDocument.create();
const newFirstPage = newPdfDoc.addPage([width * 2, height * 2]);
A function is created to embed and draw a page on a new page.
const embedAndDrawPage = async (pdfDoc, newPdfDoc, pageIndex, x, y, width, height) => {
const embeddedPage = await newPdfDoc.embedPage(pdfDoc.getPage(pageIndex));
return { embeddedPage, x, y, width, height };
};
drawPagesOnNewPage function is created to draw pages on a new page.
const drawPagesOnNewPage = (newPage, pages) => {
pages.forEach(page => {
newPage.drawPage(page.embeddedPage, {
x: page.x,
y: page.y,
width: page.width,
height: page.height,
});
});
};
if (residual === 1) {
const embeddedPage = await newPdfDoc.embedPage(firstPage);
newFirstPage.drawPage(embeddedPage, {
x: width,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedPage, {
x: width,
y: 0,
width: width,
height: height,
});
The QR code is embedded on the first page.
const qrPdfDoc = await loadPdf('qr.pdf');
const [qrPage] = await newPdfDoc.copyPages(qrPdfDoc, [0]);
const embeddedQrPage = await newPdfDoc.embedPage(qrPage);
newFirstPage.drawPage(embeddedQrPage, {
x: 0,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedQrPage, {
x: 0,
y: 0,
width: width,
height: height,
});
newPdfDoc.addPage([width * 2, height * 2]);
Depending on the number of pages, the pages are drawn on the new page.
if (numPages > 1) {
i = 1;
n = numPages - 1;
while (i < n / 2) {
const newFrontPage = newPdfDoc.addPage([width * 2, height * 2]);
const frontPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, n + 1 - i, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n + 1 - i, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i, width, 0, width, height),
];
drawPagesOnNewPage(newFrontPage, frontPages);
const newBackPage = newPdfDoc.addPage([width * 2, height * 2]);
const backPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, width, 0, width, height),
];
drawPagesOnNewPage(newBackPage, backPages);
i += 2;
}
}
If the residual is 2, the pages are drawn on the new page.
} else if (residual === 2) {
const embeddedPage1 = await newPdfDoc.embedPage(firstPage);
newFirstPage.drawPage(embeddedPage1, {
x: width,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedPage1, {
x: width,
y: 0,
width: width,
height: height,
});
The QR code is embedded on the first page.
const qrPdfDoc = await loadPdf('qr.pdf');
const [qrPage] = await newPdfDoc.copyPages(qrPdfDoc, [0]);
According to the number of pages, the pages are drawn on the new page.
if (numPages > 1) {
i = 1;
n = numPages - 1;
while (i < n / 2) {
const newFrontPage = newPdfDoc.addPage([width * 2, height * 2]);
const frontPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, n + 1 - i, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n + 1 - i, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i, width, 0, width, height),
];
drawPagesOnNewPage(newFrontPage, frontPages);
const newBackPage = newPdfDoc.addPage([width * 2, height * 2]);
const backPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, width, 0, width, height),
];
drawPagesOnNewPage(newBackPage, backPages);
i += 2;
}
}
If the residual is 2, the pages are drawn on the new page.
} else if (residual === 2) {
const embeddedPage1 = await newPdfDoc.embedPage(firstPage);
newFirstPage.drawPage(embeddedPage1, {
x: width,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedPage1, {
x: width,
y: 0,
width: width,
height: height,
});
The QR code is embedded on the first page.
const qrPdfDoc = await loadPdf('qr.pdf');
const [qrPage] = await newPdfDoc.copyPages(qrPdfDoc, [0]);
const embeddedQrPage = await newPdfDoc.embedPage(qrPage);
newFirstPage.drawPage(embeddedQrPage, {
x: 0,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedQrPage, {
x: 0,
y: 0,
width: width,
height: height,
});
The pages are drawn on the new page.
const embeddedPage2 = await newPdfDoc.embedPage(lastPage);
const newSecondPage = newPdfDoc.addPage([width * 2, height * 2]);
newSecondPage.drawPage(embeddedPage2, {
x: width,
y: height,
width: width,
height: height,
});
newSecondPage.drawPage(embeddedPage2, {
x: width,
y: 0,
width: width,
height: height,
});
If the num of pages is greater than 1, the pages are drawn on the new page.
if (numPages > 2) {
i = 1;
n = numPages - 1;
while (i < n / 2) {
const newFrontPage = newPdfDoc.addPage([width * 2, height * 2]);
const frontPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i, width, 0, width, height),
];
drawPagesOnNewPage(newFrontPage, frontPages);
const newBackPage = newPdfDoc.addPage([width * 2, height * 2]);
const backPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - 1 - i, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - 1 - i, width, 0, width, height),
];
drawPagesOnNewPage(newBackPage, backPages);
i += 2;
}
}
If the residual is 3, the pages are drawn on the new page.
} else if (residual === 3) {
const embeddedPage1 = await newPdfDoc.embedPage(firstPage);
newFirstPage.drawPage(embeddedPage1, {
x: width,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedPage1, {
x: width,
y: 0,
width: width,
height: height,
});
The QR code is embedded on the first page.
const qrPdfDoc = await loadPdf('qr.pdf');
const [qrPage] = await newPdfDoc.copyPages(qrPdfDoc, [0]);
const embeddedQrPage = await newPdfDoc.embedPage(qrPage);
newFirstPage.drawPage(embeddedQrPage, {
x: 0,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedQrPage, {
x: 0,
y: 0,
width: width,
height: height,
});
The pages are drawn on the new page.
const embeddedPage2 = await newPdfDoc.embedPage(secondPage);
const newSecondPage = newPdfDoc.addPage([width * 2, height * 2]);
newSecondPage.drawPage(embeddedPage2, {
x: 0,
y: height,
width: width,
height: height,
});
newSecondPage.drawPage(embeddedPage2, {
x: 0,
y: 0,
width: width,
height: height,
});
const embeddedPage3 = await newPdfDoc.embedPage(lastPage);
newSecondPage.drawPage(embeddedPage3, {
x: width,
y: height,
width: width,
height: height,
});
newSecondPage.drawPage(embeddedPage3, {
x: width,
y: 0,
width: width,
height: height,
});
If the num of pages is greater than 1, the pages are drawn on the new page.
if (numPages > 1) {
i = 1;
n = numPages - 1;
while (i < n / 2) {
const newFrontPage = newPdfDoc.addPage([width * 2, height * 2]);
const frontPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, width, 0, width, height),
];
drawPagesOnNewPage(newFrontPage, frontPages);
const newBackPage = newPdfDoc.addPage([width * 2, height * 2]);
const backPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 2, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - 1 - i, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 2, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - 1 - i, width, 0, width, height),
];
drawPagesOnNewPage(newBackPage, backPages);
i += 2;
}
}
If the residual is 0, the pages are drawn on the new page.
} else if (residual === 0) {
const embeddedPage1 = await newPdfDoc.embedPage(firstPage);
newFirstPage.drawPage(embeddedPage1, {
x: width,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedPage1, {
x: width,
y: 0,
width: width,
height: height,
});
const embeddedPage2 = await newPdfDoc.embedPage(lastPage); // Embed the last page
newFirstPage.drawPage(embeddedPage2, {
x: 0,
y: height,
width: width,
height: height,
});
newFirstPage.drawPage(embeddedPage2, {
x: 0,
y: 0,
width: width,
height: height,
});
const embeddedPage3 = await newPdfDoc.embedPage(secondPage);
The url of the PNG image is defined and the PNG image is loaded.
const pngUrl = 'url.png';
const pngBytes = await loadPngImage(pngUrl);
const pngImage = await newPdfDoc.embedPng(pngBytes);
const pngDims = pngImage.scale(0.6);
const centerX = (firstPage.getWidth() + (pngDims.height / 2));
const centerY = (firstPage.getHeight() - pngDims.width) / 2;
newFirstPage.drawImage(pngImage, {
x: centerX,
y: centerY,
width: pngDims.width,
height: pngDims.height,
rotate: PDFLib.degrees(90),
});
const newCenterY = (firstPage.getHeight()) + (firstPage.getHeight() - pngDims.width) / 2;
newFirstPage.drawImage(pngImage, {
x: centerX,
y: newCenterY,
width: pngDims.width,
height: pngDims.height,
rotate: PDFLib.degrees(90),
});
The pages are drawn on the new page.
const newSecondPage = newPdfDoc.addPage([width * 2, height * 2]);
newSecondPage.drawPage(embeddedPage3, {
x: 0,
y: height,
width: width,
height: height,
});
newSecondPage.drawPage(embeddedPage3, {
x: 0,
y: 0,
width: width,
height: height,
});
const embeddedPage4 = await newPdfDoc.embedPage(penultimatePage);
newSecondPage.drawPage(embeddedPage4, {
x: width,
y: height,
width: width,
height: height,
});
newSecondPage.drawPage(embeddedPage4, {
x: width,
y: 0,
width: width,
height: height,
});
When the number of pages is greater than 4, the pages are drawn on the new page.
if (numPages > 4) {
i = 1;
n = numPages - 2;
while (i < n / 2) {
const newFrontPage = newPdfDoc.addPage([width * 2, height * 2]);
const frontPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - i, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 1, width, 0, width, height),
];
drawPagesOnNewPage(newFrontPage, frontPages);
const newBackPage = newPdfDoc.addPage([width * 2, height * 2]);
const backPages = [
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 2, 0, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - 1 - i, width, height, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, i + 2, 0, 0, width, height),
await embedAndDrawPage(pdfDoc, newPdfDoc, n - 1 - i, width, 0, width, height),
];
drawPagesOnNewPage(newBackPage, backPages);
i += 2;
}
}
}
The PDF is saved and the download button is updated.
const pdfBytes = await newPdfDoc.save();
console.log('PDF saved');
const uploadText = document.getElementById('uploadText');
uploadText.innerText = `${file.name}`;
uploadText.classList.remove('text-gray-700');
uploadText.classList.add('text-blue-500');
const uploadMax = document.getElementById('uploadMax');
uploadMax.innerText = 'Para cambiar da click o arrastra un nuevo archivo.';
const downloadButton = document.getElementById('downloadButton');
const spinnerButton = document.createElement('button');
spinnerButton.disabled = true;
spinnerButton.type = 'button';
spinnerButton.className = 'btn btn-primary bg-blue-300 text-white text-xl px-8 py-2 rounded cursor-not-allowed font-bold';
spinnerButton.innerHTML = `
<svg aria-hidden="true" role="status" class="inline w-4 h-4 me-3 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"/>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"/>
</svg>
Cargando...
`;
downloadButton.parentNode.replaceChild(spinnerButton, downloadButton);
setTimeout(() => {
spinnerButton.parentNode.replaceChild(downloadButton, spinnerButton);
downloadButton.disabled = false;
downloadButton.classList.remove('bg-gray-400', 'cursor-not-allowed');
downloadButton.classList.add('bg-blue-500', 'hover:bg-blue-700');
downloadButton.style.display = 'block';
downloadButton.onclick = () => {
const originalFileName = file.name.replace(/\.[^/.]+$/, "");
const newFileName = `${originalFileName} - (libritos.arias.pw).pdf`;
download(pdfBytes, newFileName, 'application/pdf');
};
}, 4000);
const uploadIcon = document.getElementById('upload-icon');
uploadIcon.innerHTML = `
<svg id="upload-icon" class="w-14 h-14 mb-6 text-blue-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 17V2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H3a2 2 0 0 0-2 2Zm0 0a2 2 0 0 0 2 2h12M5 15V1m8 18v-4"/>
</svg>
`;
The console will display a message when the download button is updated.
console.log('Download button updated');
}
If an error occurs, the console will display an error message.
} catch (error) {
console.error('Error processing PDF:', error);
}
});
Conclusion
I hope libritos is useful for generating book-sized PDFs from full-sized PDFs. This project leverages the pdf-lib and downloadjs libraries to achieve its functionality. Feel free to contribute to the project on GitHub