JPG to PDF
Convert JPG and PNG images into a single PDF file.
or drop images here
Your PDF is ready!
Merge PDF files
Combine multiple PDFs into one, in the order you choose.
or drop PDFs here
Convert JPG and PNG images into a single PDF file.
or drop images here
Combine multiple PDFs into one, in the order you choose.
or drop PDFs here
${item.file.name}
`; filesQueue.appendChild(card); }); filesQueue.querySelectorAll('.remove-btn').forEach(btn => { btn.addEventListener('click', () => { const id = Number(btn.dataset.id); imageFiles = imageFiles.filter(f => f.id !== id); renderQueue(); }); }); filesQueue.querySelectorAll('.rotate-btn').forEach(btn => { btn.addEventListener('click', () => { const id = Number(btn.dataset.id); const item = imageFiles.find(f => f.id === id); item.rotation = (item.rotation + 90) % 360; const img = filesQueue.querySelector(`.file-card[data-id="${id}"] img`); if (img) img.style.transform = `rotate(${item.rotation}deg)`; }); }); } new Sortable(filesQueue, { animation: 150, onEnd: () => { const newOrder = Array.from(filesQueue.children).map(c => Number(c.dataset.id)); imageFiles = newOrder.map(id => imageFiles.find(f => f.id === id)); renderQueue(); } }); function rotatePixels(file, url, angle) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const swap = (angle === 90 || angle === 270); canvas.width = swap ? img.naturalHeight : img.naturalWidth; canvas.height = swap ? img.naturalWidth : img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.translate(canvas.width / 2, canvas.height / 2); ctx.rotate(angle * Math.PI / 180); ctx.drawImage(img, -img.naturalWidth / 2, -img.naturalHeight / 2); const isPng = file.type === 'image/png'; canvas.toBlob(blob => { if (!blob) { file.arrayBuffer().then(bytes => resolve({ bytes, isPng })).catch(reject); return; } blob.arrayBuffer().then(bytes => resolve({ bytes, isPng })).catch(reject); }, isPng ? 'image/png' : 'image/jpeg', 0.95); }; img.onerror = () => { file.arrayBuffer().then(bytes => resolve({ bytes, isPng: file.type === 'image/png' })).catch(reject); }; img.src = url; }); } async function addImageToDoc(pdfDoc, item) { const { bytes, isPng } = await rotatePixels(item.file, item.url, item.rotation); const img = isPng ? await pdfDoc.embedPng(bytes) : await pdfDoc.embedJpg(bytes); const pageSizeKey = pageSizeSelect.value; const margin = MARGINS[marginKey]; let pageW, pageH; if (pageSizeKey === 'original') { pageW = img.width + margin * 2; pageH = img.height + margin * 2; } else { const base = PAGE_SIZES[pageSizeKey]; pageW = orientation === 'landscape' ? Math.max(base.w, base.h) : Math.min(base.w, base.h); pageH = orientation === 'landscape' ? Math.min(base.w, base.h) : Math.max(base.w, base.h); } const availW = pageW - margin * 2; const availH = pageH - margin * 2; const scale = Math.min(availW / img.width, availH / img.height); const drawW = img.width * scale; const drawH = img.height * scale; const x = margin + (availW - drawW) / 2; const y = margin + (availH - drawH) / 2; const page = pdfDoc.addPage([pageW, pageH]); page.drawImage(img, { x, y, width: drawW, height: drawH }); } function triggerDownload(blobUrl, filename) { const tempLink = document.createElement('a'); tempLink.href = blobUrl; tempLink.download = filename; document.body.appendChild(tempLink); tempLink.click(); document.body.removeChild(tempLink); } convertBtn.addEventListener('click', async () => { if (imageFiles.length === 0) return; convertBtn.disabled = true; convertBtn.innerHTML = ' Converting...'; try { if (mergeImagesCheckbox.checked || imageFiles.length === 1) { const pdfDoc = await PDFDocument.create(); for (const item of imageFiles) { await addImageToDoc(pdfDoc, item); } const pdfBytes = await pdfDoc.save(); const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const blobUrl = URL.createObjectURL(blob); downloadLink.href = blobUrl; downloadLink.setAttribute('download', 'converted.pdf'); triggerDownload(blobUrl, 'converted.pdf'); } else { const zip = new JSZip(); let i = 1; for (const item of imageFiles) { const pdfDoc = await PDFDocument.create(); await addImageToDoc(pdfDoc, item); const pdfBytes = await pdfDoc.save(); const baseName = item.file.name.replace(/\.[^.]+$/, ''); zip.file(`${i}_${baseName}.pdf`, pdfBytes); i++; } const zipBlob = await zip.generateAsync({ type: 'blob' }); const blobUrl = URL.createObjectURL(zipBlob); downloadLink.href = blobUrl; downloadLink.setAttribute('download', 'converted_images.zip'); triggerDownload(blobUrl, 'converted_images.zip'); } workspaceView.classList.add('hidden'); optionsPanelJpg.classList.add('hidden'); downloadView.classList.remove('hidden'); } catch (err) { alert('Something went wrong converting your images: ' + err.message); } finally { convertBtn.disabled = false; convertBtn.innerHTML = 'Convert to PDF '; } }); window.resetJpgTool = function () { imageFiles.forEach(f => URL.revokeObjectURL(f.url)); imageFiles = []; downloadView.classList.add('hidden'); uploadView.classList.remove('hidden'); }; })(); /* ========================================================= TOOL 3: PDF MERGER ========================================================= */ (function () { const { PDFDocument, degrees } = PDFLib; const dropZoneMerge = document.getElementById('dropZoneMerge'); const fileInputMerge = document.getElementById('fileInputMerge'); const addMoreBtnMerge = document.getElementById('addMoreBtnMerge'); const sortBtnMerge = document.getElementById('sortBtnMerge'); const mergeBtn = document.getElementById('mergeBtn'); const uploadViewMerge = document.getElementById('uploadViewMerge'); const workspaceViewMerge = document.getElementById('workspaceViewMerge'); const downloadViewMerge = document.getElementById('downloadViewMerge'); const filesQueueMerge = document.getElementById('filesQueueMerge'); const downloadLinkMerge = document.getElementById('downloadLinkMerge'); const optionsPanelMerge = document.getElementById('optionsPanelMerge'); const fileCountBadgeMerge = document.getElementById('fileCountBadgeMerge'); let pdfFiles = []; // { id, file, thumbUrl, pageCount, rotation } let uidMerge = 0; dropZoneMerge.addEventListener('click', () => fileInputMerge.click()); addMoreBtnMerge.addEventListener('click', () => fileInputMerge.click()); fileInputMerge.addEventListener('change', (e) => addPdfFiles(e.target.files)); ['dragover', 'dragenter'].forEach(evt => dropZoneMerge.addEventListener(evt, (e) => { e.preventDefault(); dropZoneMerge.classList.add('border-red-500'); }) ); ['dragleave', 'drop'].forEach(evt => dropZoneMerge.addEventListener(evt, (e) => { e.preventDefault(); dropZoneMerge.classList.remove('border-red-500'); }) ); dropZoneMerge.addEventListener('drop', (e) => addPdfFiles(e.dataTransfer.files)); sortBtnMerge.addEventListener('click', () => { pdfFiles.sort((a, b) => a.file.name.localeCompare(b.file.name)); renderQueueMerge(); }); async function makeThumbnail(file) { const bytes = await file.arrayBuffer(); const loadingTask = pdfjsLib.getDocument({ data: bytes }); const pdf = await loadingTask.promise; const page = await pdf.getPage(1); const viewport = page.getViewport({ scale: 0.5 }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); await page.render({ canvasContext: ctx, viewport }).promise; return { thumbUrl: canvas.toDataURL('image/png'), pageCount: pdf.numPages }; } async function addPdfFiles(fileList) { const files = Array.from(fileList).filter(f => f.type === 'application/pdf'); for (const file of files) { const entry = { id: uidMerge++, file, thumbUrl: null, pageCount: null, rotation: 0 }; pdfFiles.push(entry); renderQueueMerge(); try { const { thumbUrl, pageCount } = await makeThumbnail(file); entry.thumbUrl = thumbUrl; entry.pageCount = pageCount; renderQueueMerge(); } catch (err) { console.error('Could not render thumbnail for', file.name, err); } } fileInputMerge.value = ''; } function renderQueueMerge() { if (pdfFiles.length === 0) { uploadViewMerge.classList.remove('hidden'); workspaceViewMerge.classList.add('hidden'); optionsPanelMerge.classList.add('hidden'); return; } uploadViewMerge.classList.add('hidden'); workspaceViewMerge.classList.remove('hidden'); optionsPanelMerge.classList.remove('hidden'); fileCountBadgeMerge.textContent = pdfFiles.length; filesQueueMerge.innerHTML = ''; pdfFiles.forEach((item, index) => { const card = document.createElement('div'); card.className = 'file-card bg-white rounded-lg shadow-sm p-3'; card.dataset.id = item.id; const thumbInner = item.thumbUrl ? `${item.file.name}
`; filesQueueMerge.appendChild(card); }); filesQueueMerge.querySelectorAll('.remove-btn').forEach(btn => { btn.addEventListener('click', () => { const id = Number(btn.dataset.id); pdfFiles = pdfFiles.filter(f => f.id !== id); renderQueueMerge(); }); }); filesQueueMerge.querySelectorAll('.rotate-btn').forEach(btn => { btn.addEventListener('click', () => { const id = Number(btn.dataset.id); const item = pdfFiles.find(f => f.id === id); item.rotation = (item.rotation + 90) % 360; const img = filesQueueMerge.querySelector(`.file-card[data-id="${id}"] img`); if (img) img.style.transform = `rotate(${item.rotation}deg)`; }); }); } new Sortable(filesQueueMerge, { animation: 150, onEnd: () => { const newOrder = Array.from(filesQueueMerge.children).map(c => Number(c.dataset.id)); pdfFiles = newOrder.map(id => pdfFiles.find(f => f.id === id)); renderQueueMerge(); } }); mergeBtn.addEventListener('click', async () => { if (pdfFiles.length < 2) { alert('Please add at least 2 PDF files to merge.'); return; } mergeBtn.disabled = true; mergeBtn.innerHTML = ' Merging...'; try { const mergedPdf = await PDFDocument.create(); for (const item of pdfFiles) { const bytes = await item.file.arrayBuffer(); const srcDoc = await PDFDocument.load(bytes); const copiedPages = await mergedPdf.copyPages(srcDoc, srcDoc.getPageIndices()); copiedPages.forEach(page => { if (item.rotation) { const current = page.getRotation().angle || 0; page.setRotation(degrees((current + item.rotation) % 360)); } mergedPdf.addPage(page); }); } const mergedBytes = await mergedPdf.save(); const blob = new Blob([mergedBytes], { type: 'application/pdf' }); const blobUrl = URL.createObjectURL(blob); downloadLinkMerge.href = blobUrl; const tempLink = document.createElement('a'); tempLink.href = blobUrl; tempLink.download = 'merged.pdf'; document.body.appendChild(tempLink); tempLink.click(); document.body.removeChild(tempLink); workspaceViewMerge.classList.add('hidden'); optionsPanelMerge.classList.add('hidden'); downloadViewMerge.classList.remove('hidden'); } catch (err) { alert('Something went wrong merging your PDFs: ' + err.message); } finally { mergeBtn.disabled = false; mergeBtn.innerHTML = 'Merge PDF '; } }); window.resetMergeTool = function () { pdfFiles = []; downloadViewMerge.classList.add('hidden'); uploadViewMerge.classList.remove('hidden'); }; })();