<!
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Image to PDF Converter - Windows</title>
<style>
@import url('https://fonts.googleapis.com/css2?
family=Poppins:wght@600&display=swap');
:root {
--bg-color: #ffffff;
--primary-color: #111827;
--secondary-color: #6b7280;
--accent-color: #2563eb;
--card-bg: #f9fafb;
--shadow-color: rgba(0,0,0,0.05);
--border-radius: 0.75rem;
--transition-speed: 0.3s;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background: var(--bg-color);
color: var(--primary-color);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
position: sticky;
top: 0;
background: var(--bg-color);
border-bottom: 1px solid #e5e7eb;
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 10;
}
header h1 {
font-size: 1.75rem;
font-weight: 700;
margin: 0;
color: var(--primary-color);
}
main {
max-width: 800px;
margin: 2rem auto 4rem;
padding: 0 1rem;
flex-grow: 1;
}
.hero {
text-align: center;
margin-bottom: 3rem;
}
.hero h2 {
font-size: 2.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.hero p {
font-weight: 500;
color: var(--secondary-color);
font-size: 1.125rem;
margin-top: 0;
}
.upload-section {
background: var(--card-bg);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: 0 4px 6px var(--shadow-color);
display: flex;
flex-direction: column;
gap: 1.5rem;
}
label.button {
background-color: var(--accent-color);
color: white;
padding: 0.75rem 1.5rem;
font-weight: 600;
font-size: 1rem;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
transition: background-color var(--transition-speed);
display: inline-block;
text-align: center;
user-select: none;
align-self: center;
}
label.button:hover {
background-color: #1d4ed8;
}
input[type="file"] {
display: none;
}
#preview {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(100px,1fr));
gap: 1rem;
max-height: 300px;
overflow-y: auto;
padding: 0.25rem;
}
#preview img {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: var(--border-radius);
box-shadow: 0 2px 4px var(--shadow-color);
transition: transform var(--transition-speed);
cursor: default;
}
#preview img:hover {
transform: scale(1.05);
}
.actions {
display: flex;
justify-content: center;
gap: 1rem;
}
button.convert-btn {
background-color: var(--accent-color);
color: white;
padding: 0.75rem 2rem;
font-weight: 700;
font-size: 1.125rem;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
transition: background-color var(--transition-speed);
user-select: none;
}
button.convert-btn:disabled {
background-color: #a5b4fc;
cursor: not-allowed;
}
button.convert-btn:hover:not(:disabled) {
background-color: #1e40af;
}
footer {
text-align: center;
color: var(--secondary-color);
padding: 1rem 0;
font-size: 0.875rem;
}
/* Scrollbar styling for preview */
#preview::-webkit-scrollbar {
height: 8px;
}
#preview::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: var(--border-radius);
}
#preview::-webkit-scrollbar-track {
background-color: transparent;
}
</style>
</head>
<body>
<header>
<h1>Image to PDF Converter</h1>
</header>
<main>
<section class="hero" aria-label="Application introduction">
<h2>Convert your images into a PDF</h2>
<p>Select multiple images and combine them into a single downloadable PDF
file.</p>
</section>
<section class="upload-section" aria-label="Image selection and preview">
<label for="image-input" class="button" role="button" tabindex="0" aria-
controls="preview" aria-describedby="upload-instruction">
Select Images
</label>
<input type="file" id="image-input" accept="image/*" multiple aria-
describedby="upload-instruction" />
<div id="preview" aria-live="polite" aria-atomic="true" aria-
relevant="additions removals" aria-label="Selected image previews"></div>
<div class="actions">
<button id="convert-btn" class="convert-btn" disabled>Convert to PDF</button>
</div>
</section>
</main>
<footer>
© 2024 Image to PDF Converter
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"
crossorigin="anonymous"></script>
<script>
(() => {
const { jsPDF } = window.jspdf;
const imageInput = document.getElementById('image-input');
const preview = document.getElementById('preview');
const convertBtn = document.getElementById('convert-btn');
let images = [];
function resetPreview() {
preview.innerHTML = '';
images = [];
convertBtn.disabled = true;
}
function addImagePreview(file) {
const imgElement = document.createElement('img');
imgElement.alt = file.name;
const reader = new FileReader();
reader.onload = (e) => {
imgElement.src = e.target.result;
};
reader.readAsDataURL(file);
preview.appendChild(imgElement);
images.push({ file, dataUrl: null });
// We will assign dataUrl later when loading fully
}
// Read all files and get their DataURLs for PDF embedding
async function loadImages(files) {
const loadedImages = [];
for (const file of files) {
loadedImages.push(await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve({ file, dataUrl: reader.result });
reader.onerror = () => reject(new Error("Failed to read image: " +
file.name));
reader.readAsDataURL(file);
}));
}
return loadedImages;
}
imageInput.addEventListener('change', async (e) => {
resetPreview();
const files = [...e.target.files];
if (files.length === 0) return;
// Show previews immediately
files.forEach(addImagePreview);
// Load all images data URLs for pdf later
images = await loadImages(files);
convertBtn.disabled = images.length === 0;
});
function imgToPdfDimensions(imgWidth, imgHeight, pdfWidth, pdfHeight) {
// Calculate scaled dimensions keeping aspect ratio inside the pdf page
const widthRatio = pdfWidth / imgWidth;
const heightRatio = pdfHeight / imgHeight;
const scale = Math.min(widthRatio, heightRatio);
return {
width: imgWidth * scale,
height: imgHeight * scale,
};
}
convertBtn.addEventListener('click', async () => {
if (images.length === 0) return;
convertBtn.disabled = true;
convertBtn.textContent = "Converting...";
// Create a pdf instance - standard A4 in points (210mm x 297mm approx)
const pdf = new jsPDF({
unit: 'pt',
format: 'a4',
compress: true,
});
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
for (let i = 0; i < images.length; i++) {
const img = new Image();
img.src = images[i].dataUrl;
// Wait for image to load to get dimensions
await new Promise((resolve) => {
img.onload = () => {
const { width, height } = imgToPdfDimensions(img.width, img.height,
pdfWidth, pdfHeight);
// Center image in pdf page
const x = (pdfWidth - width) / 2;
const y = (pdfHeight - height) / 2;
pdf.addImage(img, 'JPEG', x, y, width, height);
if (i < images.length - 1) pdf.addPage();
resolve();
};
img.onerror = () => {
alert("Failed to load an image for PDF, skipping.");
resolve();
};
});
}
pdf.save('images.pdf');
convertBtn.textContent = "Convert to PDF";
convertBtn.disabled = false;
});
})();
</script>
</body>
</html>