Contact — image ⇄ PDF converter

client-side photo lab

CONTACT

Lay your images down on a contact sheet and pull out a PDF — or split a PDF back into individual prints. Everything happens here; nothing is saved in our server.

Drop images here to build a sheet, or click to browse

JPEG · PNG · WEBP · GIF · BMP · SVG — combined into one PDF, in the order shown below

Drop a PDF here to pull its pages out, or click to browse

Pages become individual PNG, JPEG or WEBP frames

No file ever saves in our server.

' + '
' + escapeHtml(f.name) + '
' + dims + '
' + '
' + '' + '' + '' + '
' + '
' ); }).join(''); } function moveFrame(id, dir){ var i = frames.findIndex(function(f){ return f.id === id; }); if (i < 0) return; var j = i + dir; if (j < 0 || j >= frames.length) return; var tmp = frames[i]; frames[i] = frames[j]; frames[j] = tmp; renderFrameGrid(); scheduleRebuildPdf(0); } function removeFrame(id){ var f = frames.find(function(x){ return x.id === id; }); if (f && f.objectURL) URL.revokeObjectURL(f.objectURL); frames = frames.filter(function(x){ return x.id !== id; }); renderFrameGrid(); if (frames.length === 0){ document.getElementById('img2pdfBody').style.display = 'none'; document.getElementById('downloadPdfBtn').disabled = true; if (img2pdf.pdfURL){ URL.revokeObjectURL(img2pdf.pdfURL); img2pdf.pdfURL = null; } document.getElementById('buildStatus').textContent = 'No images yet'; return; } scheduleRebuildPdf(0); } function scheduleRebuildPdf(delay){ clearTimeout(img2pdf.debounceTimer); document.getElementById('buildStatus').textContent = 'Building sheet…'; img2pdf.debounceTimer = setTimeout(buildPdf, delay); } function getPageDims(frame){ var s = img2pdf.settings; if (s.pageSize === 'fit'){ return { w: frame.naturalWidth, h: frame.naturalHeight, unit: 'px', margin: 0 }; } var base = PAGE_SIZES[s.pageSize]; var orient = s.orientation; if (orient === 'auto') orient = (frame.naturalWidth > frame.naturalHeight) ? 'landscape' : 'portrait'; var w, h; if (orient === 'landscape'){ w = Math.max(base.w, base.h); h = Math.min(base.w, base.h); } else { w = Math.min(base.w, base.h); h = Math.max(base.w, base.h); } return { w: w, h: h, unit: 'pt', margin: parseInt(s.margin, 10) || 0 }; } function rasterizeForPdf(frame){ var canvas = document.createElement('canvas'); canvas.width = frame.naturalWidth; canvas.height = frame.naturalHeight; var ctx = canvas.getContext('2d'); var fmt = img2pdf.settings.embed; if (fmt === 'jpeg'){ ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); } ctx.drawImage(frame.img, 0, 0); if (fmt === 'jpeg'){ var q = QUALITY_STOPS[img2pdf.settings.qIndex].q / 100; return canvas.toDataURL('image/jpeg', q); } return canvas.toDataURL('image/png'); } function buildPdf(){ var statusEl = document.getElementById('buildStatus'); var dlBtn = document.getElementById('downloadPdfBtn'); if (frames.length === 0 || frames.some(function(f){ return !f.img; })){ if (frames.length === 0) statusEl.textContent = 'No images yet'; return; } if (typeof window.jspdf === 'undefined'){ statusEl.textContent = 'PDF library failed to load — check your connection.'; return; } try{ var doc = null; var fmt = img2pdf.settings.embed; frames.forEach(function(frame, idx){ var dims = getPageDims(frame); var dataURL = rasterizeForPdf(frame); var availW = dims.w - dims.margin * 2; var availH = dims.h - dims.margin * 2; var scale = Math.min(availW / frame.naturalWidth, availH / frame.naturalHeight); var drawW = frame.naturalWidth * scale; var drawH = frame.naturalHeight * scale; var x = (dims.w - drawW) / 2; var y = (dims.h - drawH) / 2; var orientArg = dims.w > dims.h ? 'l' : 'p'; if (idx === 0){ doc = new window.jspdf.jsPDF({ orientation: orientArg, unit: dims.unit, format: [dims.w, dims.h] }); } else { doc.addPage([dims.w, dims.h], orientArg); } doc.addImage(dataURL, fmt === 'jpeg' ? 'JPEG' : 'PNG', x, y, drawW, drawH); }); var blob = doc.output('blob'); if (img2pdf.pdfURL) URL.revokeObjectURL(img2pdf.pdfURL); img2pdf.pdfURL = URL.createObjectURL(blob); statusEl.innerHTML = '' + frames.length + (frames.length === 1 ? ' page' : ' pages') + ' · ' + formatBytes(blob.size); dlBtn.disabled = false; } catch(e){ statusEl.textContent = 'Could not build the PDF: ' + e.message; } } function wireImg2Pdf(){ var dz = document.getElementById('dzImages'); var input = document.getElementById('imgInput'); dz.addEventListener('click', function(){ input.click(); }); dz.addEventListener('keydown', function(e){ if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); input.click(); } }); input.addEventListener('change', function(){ Array.from(input.files).filter(function(f){ return f.type.indexOf('image/') === 0; }).forEach(addImageFrame); input.value = ''; }); ['dragenter','dragover'].forEach(function(evt){ dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.add('active'); }); }); ['dragleave','drop'].forEach(function(evt){ dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.remove('active'); }); }); dz.addEventListener('drop', function(e){ if (e.dataTransfer && e.dataTransfer.files){ Array.from(e.dataTransfer.files).filter(function(f){ return f.type.indexOf('image/') === 0; }).forEach(addImageFrame); } }); document.getElementById('frameGrid').addEventListener('click', function(e){ var btn = e.target.closest('button[data-action]'); if (!btn) return; var frameEl = e.target.closest('.frame'); var id = frameEl.dataset.id; var action = btn.dataset.action; if (action === 'left') moveFrame(id, -1); else if (action === 'right') moveFrame(id, 1); else if (action === 'remove') removeFrame(id); }); document.getElementById('pageSize').addEventListener('change', function(e){ img2pdf.settings.pageSize = e.target.value; var isFit = img2pdf.settings.pageSize === 'fit'; document.getElementById('orientationField').style.opacity = isFit ? 0.4 : 1; document.getElementById('orientation').disabled = isFit; document.getElementById('marginField').style.opacity = isFit ? 0.4 : 1; document.getElementById('margin').disabled = isFit; scheduleRebuildPdf(0); }); document.getElementById('orientation').addEventListener('change', function(e){ img2pdf.settings.orientation = e.target.value; scheduleRebuildPdf(0); }); document.getElementById('margin').addEventListener('change', function(e){ img2pdf.settings.margin = e.target.value; scheduleRebuildPdf(0); }); var embedStops = document.getElementById('embedStops'); embedStops.innerHTML = QUALITY_STOPS.map(function(s, i){ return '' + s.label + ''; }).join(''); function updateEmbedQLabel(){ var s = QUALITY_STOPS[img2pdf.settings.qIndex]; document.getElementById('embedQLabel').textContent = s.label + ' · ' + s.q + '% · ' + s.desc; embedStops.querySelectorAll('span').forEach(function(sp){ sp.classList.toggle('active', parseInt(sp.dataset.i, 10) === img2pdf.settings.qIndex); }); } updateEmbedQLabel(); document.getElementById('embedQuality').addEventListener('input', function(e){ img2pdf.settings.qIndex = parseInt(e.target.value, 10); updateEmbedQLabel(); scheduleRebuildPdf(300); }); document.querySelectorAll('#embedChips .chip').forEach(function(chip){ chip.addEventListener('click', function(){ document.querySelectorAll('#embedChips .chip').forEach(function(c){ c.classList.remove('active'); }); chip.classList.add('active'); img2pdf.settings.embed = chip.dataset.embed; var qBlock = document.getElementById('embedQualityBlock'); qBlock.style.opacity = img2pdf.settings.embed === 'png' ? 0.4 : 1; document.getElementById('embedQuality').disabled = img2pdf.settings.embed === 'png'; scheduleRebuildPdf(0); }); }); document.getElementById('downloadPdfBtn').addEventListener('click', function(){ if (!img2pdf.pdfURL) return; triggerDownload(img2pdf.pdfURL, 'contact-sheet.pdf'); }); } // =================================================================== // MODE 2: PDF -> IMAGES // =================================================================== var pdfCards = new Map(); function parsePageRange(text, maxPages){ text = (text || '').trim(); if (!text) { var all = []; for (var i = 1; i <= maxPages; i++) all.push(i); return all; } var set = {}; text.split(',').forEach(function(tok){ tok = tok.trim(); if (!tok) return; if (tok.indexOf('-') > -1){ var parts = tok.split('-'); var a = parseInt(parts[0], 10), b = parseInt(parts[1], 10); if (isNaN(a) || isNaN(b)) return; if (a > b){ var t = a; a = b; b = t; } for (var n = Math.max(1, a); n <= Math.min(maxPages, b); n++) set[n] = true; } else { var v = parseInt(tok, 10); if (!isNaN(v) && v >= 1 && v <= maxPages) set[v] = true; } }); return Object.keys(set).map(Number).sort(function(a, b){ return a - b; }); } function renderPdfPageToBlob(pdfDoc, pageNum, scale, format, quality){ return pdfDoc.getPage(pageNum).then(function(page){ var viewport = page.getViewport({ scale: scale }); var canvas = document.createElement('canvas'); canvas.width = Math.ceil(viewport.width); canvas.height = Math.ceil(viewport.height); var ctx = canvas.getContext('2d'); if (format === 'jpeg'){ ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); } return page.render({ canvasContext: ctx, viewport: viewport }).promise.then(function(){ return new Promise(function(resolve, reject){ var mime = MIME[format]; var q = format === 'png' ? undefined : (quality / 100); canvas.toBlob(function(blob){ if (blob) resolve(blob); else reject(new Error('encode failed')); }, mime, q); }); }); }); } function addPdfFile(file){ var id = uid(); var card = { id: id, file: file, name: file.name, base: baseName(file.name), size: file.size, pdfDoc: null, numPages: 0, settings: { format: 'jpeg', qIndex: 2, resIndex: 1, range: '' }, pageBlobs: {}, el: null, errored: false, extracting: false, previewTimer: null }; pdfCards.set(id, card); var el = document.createElement('div'); el.className = 'pdf-card'; el.dataset.id = id; el.innerHTML = '' + '

' + escapeHtml(file.name) + '

' + '

Reading PDF…

'; document.getElementById('pdfCards').appendChild(el); card.el = el; el.querySelector('[data-action="remove"]').addEventListener('click', function(){ removePdfCard(id); }); file.arrayBuffer().then(function(buf){ if (typeof pdfjsLib === 'undefined') throw new Error('pdf.js failed to load'); return pdfjsLib.getDocument({ data: buf }).promise; }).then(function(pdfDoc){ card.pdfDoc = pdfDoc; card.numPages = pdfDoc.numPages; buildPdfCardBody(card); computePagePreviewSizes(card); }).catch(function(err){ card.errored = true; el.innerHTML = '' + '

' + escapeHtml(file.name) + '

' + '

Couldn\'t open this PDF — it may be encrypted, corrupted, or not a valid PDF file.

'; el.querySelector('[data-action="remove"]').addEventListener('click', function(){ removePdfCard(id); }); }); } function buildPdfCardBody(card){ var el = card.el; var chips = ['jpeg','png','webp'].map(function(fmt){ var supported = SUPPORTED[fmt]; return ''; }).join(''); var qStops = QUALITY_STOPS.map(function(s, i){ return '' + s.label + ''; }).join(''); var rStops = RES_STOPS.map(function(s, i){ return '' + s.label + '
' + s.dpi + '
'; }).join(''); el.innerHTML = '
' + '
' + '

' + escapeHtml(card.name) + '

' + '
' + 'PDF' + '' + card.numPages + (card.numPages === 1 ? ' page' : ' pages') + '' + '' + formatBytes(card.size) + '' + '
' + '
' + '' + '
' + '
' + '
' + '
Save as
' + '
' + chips + '
' + '
' + '
' + '
Compression
' + '' + '
' + qStops + '
' + '
' + '
' + '
' + '
' + '
Resolution
' + '' + '
' + rStops + '
' + '
' + '
' + '
Pages
' + '' + '
' + '
' + '
' + '
' + '' + '' + '
' + '

' + '
'; el.querySelector('[data-action="remove"]').addEventListener('click', function(){ removePdfCard(card.id); }); function updateQLabel(){ var s = QUALITY_STOPS[card.settings.qIndex]; el.querySelector('[data-role="qlabel"]').textContent = s.label + ' · ' + s.q + '% · ' + s.desc; } function updateRLabel(){ var s = RES_STOPS[card.settings.resIndex]; el.querySelector('[data-role="rlabel"]').textContent = s.label + ' · ' + s.dpi; } updateQLabel(); updateRLabel(); el.querySelector('[data-role="quality-block"]').style.opacity = card.settings.format === 'png' ? 0.4 : 1; el.querySelector('[data-role="quality"]').disabled = card.settings.format === 'png'; el.querySelectorAll('[data-role="fmt-chips"] .chip').forEach(function(chip){ chip.addEventListener('click', function(){ if (chip.disabled) return; el.querySelectorAll('[data-role="fmt-chips"] .chip').forEach(function(c){ c.classList.remove('active'); }); chip.classList.add('active'); card.settings.format = chip.dataset.fmt; el.querySelector('[data-role="quality-block"]').style.opacity = card.settings.format === 'png' ? 0.4 : 1; el.querySelector('[data-role="quality"]').disabled = card.settings.format === 'png'; schedulePreview(card, 0); }); }); el.querySelector('[data-role="quality"]').addEventListener('input', function(e){ card.settings.qIndex = parseInt(e.target.value, 10); updateQLabel(); schedulePreview(card, 300); }); el.querySelector('[data-role="resolution"]').addEventListener('input', function(e){ card.settings.resIndex = parseInt(e.target.value, 10); updateRLabel(); schedulePreview(card, 300); }); el.querySelector('[data-role="range"]').addEventListener('input', function(e){ card.settings.range = e.target.value; }); el.querySelector('[data-role="extract"]').addEventListener('click', function(){ extractPages(card); }); el.querySelector('[data-role="dlzip"]').addEventListener('click', function(){ downloadCardZip(card); }); } function schedulePreview(card, delay){ clearTimeout(card.previewTimer); card.previewTimer = setTimeout(function(){ computePagePreviewSizes(card); }, delay); } function computePagePreviewSizes(card){ if (!card.pdfDoc) return; var el = card.el; var scale = RES_STOPS[card.settings.resIndex].scale; var q = QUALITY_STOPS[card.settings.qIndex].q; var fmts = ['jpeg','png','webp'].filter(function(f){ return SUPPORTED[f]; }); Promise.all(fmts.map(function(fmt){ return renderPdfPageToBlob(card.pdfDoc, 1, scale, fmt, q) .then(function(blob){ return { fmt: fmt, blob: blob }; }) .catch(function(){ return { fmt: fmt, blob: null }; }); })).then(function(results){ var smallestFmt = null, smallestSize = Infinity; results.forEach(function(r){ if (r.blob && r.blob.size < smallestSize){ smallestSize = r.blob.size; smallestFmt = r.fmt; } }); results.forEach(function(r){ var chip = el.querySelector('[data-role="fmt-chips"] .chip[data-fmt="' + r.fmt + '"]'); if (!chip) return; var sz = chip.querySelector('.sz'); if (sz) sz.textContent = r.blob ? ('~' + formatBytes(r.blob.size) + '/pg') : 'failed'; var oldBadge = chip.querySelector('.badge'); if (oldBadge) oldBadge.remove(); if (r.fmt === smallestFmt){ var badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = 'smallest'; chip.appendChild(badge); } }); }); } function extractPages(card){ if (card.extracting || !card.pdfDoc) return; var el = card.el; var pages = parsePageRange(card.settings.range, card.numPages); var progressEl = el.querySelector('[data-role="progress"]'); var gridEl = el.querySelector('[data-role="page-grid"]'); var extractBtn = el.querySelector('[data-role="extract"]'); var zipBtn = el.querySelector('[data-role="dlzip"]'); if (pages.length === 0){ progressEl.textContent = 'No valid pages in that range.'; return; } card.extracting = true; extractBtn.disabled = true; zipBtn.style.display = 'none'; gridEl.innerHTML = ''; card.pageBlobs = {}; var scale = RES_STOPS[card.settings.resIndex].scale; var fmt = card.settings.format; var q = QUALITY_STOPS[card.settings.qIndex].q; var digits = String(card.numPages).length; var i = 0; function next(){ if (i >= pages.length){ progressEl.textContent = 'Done — ' + pages.length + (pages.length === 1 ? ' page' : ' pages') + ' extracted.'; card.extracting = false; extractBtn.disabled = false; zipBtn.style.display = ''; return; } var pageNum = pages[i]; progressEl.textContent = 'Rendering page ' + pageNum + '… (' + (i + 1) + ' of ' + pages.length + ')'; renderPdfPageToBlob(card.pdfDoc, pageNum, scale, fmt, q).then(function(blob){ card.pageBlobs[pageNum] = blob; var url = URL.createObjectURL(blob); var tile = document.createElement('div'); tile.className = 'page-tile'; tile.innerHTML = '
' + pad2(pageNum) + '
' + '
' + formatBytes(blob.size) + '
' + ''; tile.querySelector('.page-dl').addEventListener('click', function(){ triggerDownload(url, card.base + '-p' + String(pageNum).padStart(digits, '0') + '.' + EXT[fmt]); }); gridEl.appendChild(tile); i++; next(); }).catch(function(){ progressEl.textContent = 'Failed to render page ' + pageNum + '.'; i++; next(); }); } next(); } function downloadCardZip(card){ if (typeof JSZip === 'undefined'){ alert('The zip library failed to load.'); return; } var fmt = card.settings.format; var digits = String(card.numPages).length; var zip = new JSZip(); var any = false; Object.keys(card.pageBlobs).forEach(function(pageNum){ var blob = card.pageBlobs[pageNum]; if (!blob) return; zip.file(card.base + '-p' + String(pageNum).padStart(digits, '0') + '.' + EXT[fmt], blob); any = true; }); if (!any) return; zip.generateAsync({ type: 'blob' }).then(function(content){ var url = URL.createObjectURL(content); triggerDownload(url, card.base + '-pages.zip'); setTimeout(function(){ URL.revokeObjectURL(url); }, 4000); }); } function removePdfCard(id){ var card = pdfCards.get(id); if (!card) return; if (card.el && card.el.parentNode) card.el.parentNode.removeChild(card.el); pdfCards.delete(id); } function wirePdf2Img(){ var dz = document.getElementById('dzPdf'); var input = document.getElementById('pdfInput'); dz.addEventListener('click', function(){ input.click(); }); dz.addEventListener('keydown', function(e){ if (e.key === 'Enter' || e.key === ' '){ e.preventDefault(); input.click(); } }); input.addEventListener('change', function(){ Array.from(input.files).filter(function(f){ return f.type === 'application/pdf' || /\.pdf$/i.test(f.name); }).forEach(addPdfFile); input.value = ''; }); ['dragenter','dragover'].forEach(function(evt){ dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.add('active'); }); }); ['dragleave','drop'].forEach(function(evt){ dz.addEventListener(evt, function(e){ e.preventDefault(); dz.classList.remove('active'); }); }); dz.addEventListener('drop', function(e){ if (e.dataTransfer && e.dataTransfer.files){ Array.from(e.dataTransfer.files).filter(function(f){ return f.type === 'application/pdf' || /\.pdf$/i.test(f.name); }).forEach(addPdfFile); } }); } // =================================================================== // Tabs + init // =================================================================== function wireTabs(){ var tabImg = document.getElementById('tabImg2Pdf'); var tabPdf = document.getElementById('tabPdf2Img'); var panelImg = document.getElementById('panelImg2Pdf'); var panelPdf = document.getElementById('panelPdf2Img'); tabImg.addEventListener('click', function(){ tabImg.classList.add('active'); tabImg.setAttribute('aria-selected','true'); tabPdf.classList.remove('active'); tabPdf.setAttribute('aria-selected','false'); panelImg.classList.add('active'); panelPdf.classList.remove('active'); }); tabPdf.addEventListener('click', function(){ tabPdf.classList.add('active'); tabPdf.setAttribute('aria-selected','true'); tabImg.classList.remove('active'); tabImg.setAttribute('aria-selected','false'); panelPdf.classList.add('active'); panelImg.classList.remove('active'); }); } function init(){ if (typeof pdfjsLib !== 'undefined'){ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js'; } detectFormatSupport('image/webp').then(function(ok){ SUPPORTED.webp = ok; }); wireTabs(); wireImg2Pdf(); wirePdf2Img(); document.getElementById('buildStatus').textContent = 'No images yet'; } document.addEventListener('DOMContentLoaded', init); })();

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top