|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
|
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Images to PDF - Tools Collection</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; |
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); |
|
|
min-height: 100vh; |
|
|
color: #e4e4e4; |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1000px; |
|
|
margin: 0 auto; |
|
|
padding: 40px 20px; |
|
|
} |
|
|
|
|
|
.header { |
|
|
text-align: center; |
|
|
margin-bottom: 40px; |
|
|
} |
|
|
|
|
|
.back-button { |
|
|
position: absolute; |
|
|
left: 20px; |
|
|
top: 20px; |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
padding: 12px 20px; |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
|
border-radius: 8px; |
|
|
color: #e4e4e4; |
|
|
text-decoration: none; |
|
|
font-size: 0.9rem; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.back-button:hover { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
transform: translateX(-5px); |
|
|
} |
|
|
|
|
|
.back-button svg { |
|
|
width: 16px; |
|
|
height: 16px; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.logo { |
|
|
font-size: 4rem; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
font-size: 2.5rem; |
|
|
background: linear-gradient(135deg, #e94560, #f39c12); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
background-clip: text; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.subtitle { |
|
|
color: #8892b0; |
|
|
font-size: 1.1rem; |
|
|
} |
|
|
|
|
|
.tool-card { |
|
|
background: rgba(255, 255, 255, 0.05); |
|
|
border-radius: 20px; |
|
|
padding: 40px; |
|
|
backdrop-filter: blur(10px); |
|
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
|
} |
|
|
|
|
|
.upload-section { |
|
|
border: 2px dashed rgba(233, 69, 96, 0.5); |
|
|
border-radius: 15px; |
|
|
padding: 60px 40px; |
|
|
text-align: center; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.upload-section:hover { |
|
|
border-color: #e94560; |
|
|
background: rgba(233, 69, 96, 0.1); |
|
|
} |
|
|
|
|
|
.upload-section.dragover { |
|
|
border-color: #e94560; |
|
|
background: rgba(233, 69, 96, 0.15); |
|
|
transform: scale(1.02); |
|
|
} |
|
|
|
|
|
.upload-icon { |
|
|
font-size: 3rem; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
|
|
|
.upload-text { |
|
|
font-size: 1.2rem; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.upload-hint { |
|
|
color: #8892b0; |
|
|
font-size: 0.9rem; |
|
|
} |
|
|
|
|
|
#file-input { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.info-box { |
|
|
background: rgba(23, 162, 184, 0.1); |
|
|
border: 1px solid rgba(23, 162, 184, 0.3); |
|
|
border-radius: 10px; |
|
|
padding: 15px 20px; |
|
|
margin-bottom: 30px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
.info-box span { |
|
|
font-size: 1.3rem; |
|
|
} |
|
|
|
|
|
.info-box p { |
|
|
font-size: 0.9rem; |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
.files-preview { |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.preview-title { |
|
|
font-size: 1.1rem; |
|
|
margin-bottom: 15px; |
|
|
color: #e4e4e4; |
|
|
} |
|
|
|
|
|
.files-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.file-card { |
|
|
background: rgba(255, 255, 255, 0.05); |
|
|
border-radius: 10px; |
|
|
padding: 15px; |
|
|
text-align: center; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.file-card .remove-button { |
|
|
position: absolute; |
|
|
top: 5px; |
|
|
right: 5px; |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
border-radius: 50%; |
|
|
border: none; |
|
|
background: #dc3545; |
|
|
color: white; |
|
|
cursor: pointer; |
|
|
font-size: 14px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.file-preview { |
|
|
width: 100%; |
|
|
height: 100px; |
|
|
border-radius: 8px; |
|
|
overflow: hidden; |
|
|
margin-bottom: 10px; |
|
|
background: rgba(0, 0, 0, 0.2); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.file-preview img { |
|
|
max-width: 100%; |
|
|
max-height: 100%; |
|
|
object-fit: contain; |
|
|
} |
|
|
|
|
|
.file-info h4 { |
|
|
font-size: 0.8rem; |
|
|
white-space: nowrap; |
|
|
overflow: hidden; |
|
|
text-overflow: ellipsis; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
.file-info p { |
|
|
font-size: 0.7rem; |
|
|
color: #8892b0; |
|
|
} |
|
|
|
|
|
.file-status { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 5px; |
|
|
margin-top: 8px; |
|
|
} |
|
|
|
|
|
.status-indicator { |
|
|
width: 8px; |
|
|
height: 8px; |
|
|
border-radius: 50%; |
|
|
background: #28a745; |
|
|
} |
|
|
|
|
|
.status-indicator.processing { |
|
|
background: #ffc107; |
|
|
animation: pulse 1s infinite; |
|
|
} |
|
|
|
|
|
.status-indicator.error { |
|
|
background: #dc3545; |
|
|
} |
|
|
|
|
|
.action-buttons { |
|
|
display: flex; |
|
|
gap: 15px; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.btn { |
|
|
padding: 15px 30px; |
|
|
border-radius: 10px; |
|
|
border: none; |
|
|
font-size: 1rem; |
|
|
font-weight: 600; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.btn-primary { |
|
|
background: linear-gradient(135deg, #e94560, #f39c12); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-primary:hover:not(:disabled) { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 5px 20px rgba(233, 69, 96, 0.4); |
|
|
} |
|
|
|
|
|
.btn-primary:disabled { |
|
|
opacity: 0.5; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.btn-secondary { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
color: #e4e4e4; |
|
|
border: 1px solid rgba(255, 255, 255, 0.2); |
|
|
} |
|
|
|
|
|
.btn-secondary:hover { |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
} |
|
|
|
|
|
.btn-success { |
|
|
background: linear-gradient(135deg, #28a745, #20c997); |
|
|
color: white; |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.progress-section { |
|
|
display: none; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.progress-bar-container { |
|
|
height: 8px; |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
border-radius: 4px; |
|
|
overflow: hidden; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.progress-bar { |
|
|
height: 100%; |
|
|
background: linear-gradient(135deg, #e94560, #f39c12); |
|
|
width: 0%; |
|
|
transition: width 0.3s ease; |
|
|
} |
|
|
|
|
|
.progress-text { |
|
|
font-size: 0.9rem; |
|
|
color: #8892b0; |
|
|
} |
|
|
|
|
|
.error-message { |
|
|
display: none; |
|
|
background: rgba(220, 53, 69, 0.1); |
|
|
border: 1px solid rgba(220, 53, 69, 0.3); |
|
|
border-radius: 10px; |
|
|
padding: 15px 20px; |
|
|
margin-bottom: 20px; |
|
|
color: #dc3545; |
|
|
} |
|
|
|
|
|
.success-message { |
|
|
display: none; |
|
|
background: rgba(40, 167, 69, 0.1); |
|
|
border: 1px solid rgba(40, 167, 69, 0.3); |
|
|
border-radius: 10px; |
|
|
padding: 15px 20px; |
|
|
margin-bottom: 20px; |
|
|
color: #28a745; |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0% { |
|
|
transform: scale(1); |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
50% { |
|
|
transform: scale(1.05); |
|
|
opacity: 0.8; |
|
|
} |
|
|
|
|
|
100% { |
|
|
transform: scale(1); |
|
|
opacity: 1; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body> |
|
|
<a href="/" class="back-button"> |
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<path d="M19 12H5M12 19l-7-7 7-7" /> |
|
|
</svg> |
|
|
Back to Tools |
|
|
</a> |
|
|
|
|
|
<div class="container"> |
|
|
<div class="header"> |
|
|
<div class="logo">π</div> |
|
|
<h1>Images to PDF</h1> |
|
|
<p class="subtitle">Convert multiple images to a single PDF file</p> |
|
|
</div> |
|
|
|
|
|
<div class="tool-card"> |
|
|
<div class="info-box"> |
|
|
<span>βΉοΈ</span> |
|
|
<p>Upload multiple images and convert them to a single PDF. Drag to reorder before converting.</p> |
|
|
</div> |
|
|
|
|
|
<div id="error-message" class="error-message"></div> |
|
|
<div id="success-message" class="success-message"></div> |
|
|
|
|
|
<div class="upload-section" id="upload-section"> |
|
|
<div class="upload-icon">π€</div> |
|
|
<p class="upload-text">Drop images here or click to browse</p> |
|
|
<p class="upload-hint">Supports JPG, PNG, WebP, BMP, GIF, TIFF</p> |
|
|
<input type="file" id="file-input" accept="image/*" multiple> |
|
|
</div> |
|
|
|
|
|
<div class="files-preview" id="files-preview" style="display: none;"> |
|
|
<h3 class="preview-title">π Selected Images</h3> |
|
|
<div class="files-grid" id="files-grid"></div> |
|
|
</div> |
|
|
|
|
|
<div class="progress-section" id="progress-section"> |
|
|
<div class="progress-bar-container"> |
|
|
<div class="progress-bar" id="progress-bar"></div> |
|
|
</div> |
|
|
<p class="progress-text" id="progress-text">0% complete</p> |
|
|
</div> |
|
|
|
|
|
<div class="action-buttons"> |
|
|
<button class="btn btn-primary" id="convert-button" disabled> |
|
|
π Convert to PDF |
|
|
</button> |
|
|
<button class="btn btn-secondary" id="clear-button"> |
|
|
ποΈ Clear All |
|
|
</button> |
|
|
<button class="btn btn-success" id="download-button"> |
|
|
β¬οΈ Download PDF |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
let selectedFiles = []; |
|
|
let pdfUrl = null; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
setupEventListeners(); |
|
|
}); |
|
|
|
|
|
function setupEventListeners() { |
|
|
const uploadSection = document.getElementById('upload-section'); |
|
|
const fileInput = document.getElementById('file-input'); |
|
|
const convertButton = document.getElementById('convert-button'); |
|
|
const clearButton = document.getElementById('clear-button'); |
|
|
const downloadButton = document.getElementById('download-button'); |
|
|
|
|
|
uploadSection.addEventListener('click', () => fileInput.click()); |
|
|
uploadSection.addEventListener('dragover', handleDragOver); |
|
|
uploadSection.addEventListener('dragleave', handleDragLeave); |
|
|
uploadSection.addEventListener('drop', handleDrop); |
|
|
fileInput.addEventListener('change', handleFileSelect); |
|
|
|
|
|
convertButton.addEventListener('click', convertToPDF); |
|
|
clearButton.addEventListener('click', clearAllFiles); |
|
|
downloadButton.addEventListener('click', downloadPDF); |
|
|
} |
|
|
|
|
|
function showMessage(message, type = 'error') { |
|
|
const errorMsg = document.getElementById('error-message'); |
|
|
const successMsg = document.getElementById('success-message'); |
|
|
|
|
|
errorMsg.style.display = 'none'; |
|
|
successMsg.style.display = 'none'; |
|
|
|
|
|
if (type === 'error') { |
|
|
errorMsg.textContent = message; |
|
|
errorMsg.style.display = 'block'; |
|
|
} else { |
|
|
successMsg.textContent = message; |
|
|
successMsg.style.display = 'block'; |
|
|
} |
|
|
|
|
|
setTimeout(() => { |
|
|
errorMsg.style.display = 'none'; |
|
|
successMsg.style.display = 'none'; |
|
|
}, 5000); |
|
|
} |
|
|
|
|
|
function handleDragOver(e) { |
|
|
e.preventDefault(); |
|
|
e.currentTarget.classList.add('dragover'); |
|
|
} |
|
|
|
|
|
function handleDragLeave(e) { |
|
|
e.preventDefault(); |
|
|
e.currentTarget.classList.remove('dragover'); |
|
|
} |
|
|
|
|
|
function handleDrop(e) { |
|
|
e.preventDefault(); |
|
|
e.currentTarget.classList.remove('dragover'); |
|
|
const files = Array.from(e.dataTransfer.files); |
|
|
processFiles(files); |
|
|
} |
|
|
|
|
|
function handleFileSelect(e) { |
|
|
const files = Array.from(e.target.files); |
|
|
processFiles(files); |
|
|
} |
|
|
|
|
|
function generateUniqueId(file, length = 12) { |
|
|
let ext = "." + file.name.split(".")[file.name.split(".").length - 1]; |
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
|
|
return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('') + ext; |
|
|
} |
|
|
|
|
|
function processFiles(files) { |
|
|
files.forEach((file) => { |
|
|
if (!file.type.startsWith('image/')) { |
|
|
showMessage(`${file.name} is not an image file`, 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const id = generateUniqueId(file); |
|
|
const fileData = { |
|
|
id: id, |
|
|
file: file, |
|
|
name: file.name, |
|
|
size: formatFileSize(file.size), |
|
|
status: 'ready', |
|
|
preview: null |
|
|
}; |
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e) => { |
|
|
fileData.preview = e.target.result; |
|
|
updateUI(); |
|
|
}; |
|
|
reader.readAsDataURL(file); |
|
|
|
|
|
selectedFiles.push(fileData); |
|
|
}); |
|
|
|
|
|
updateUI(); |
|
|
} |
|
|
|
|
|
function formatFileSize(bytes) { |
|
|
if (bytes === 0) return '0 Bytes'; |
|
|
const k = 1024; |
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
|
} |
|
|
|
|
|
function updateUI() { |
|
|
const filesPreview = document.getElementById('files-preview'); |
|
|
const convertButton = document.getElementById('convert-button'); |
|
|
|
|
|
if (selectedFiles.length > 0) { |
|
|
filesPreview.style.display = 'block'; |
|
|
convertButton.disabled = false; |
|
|
renderFileCards(); |
|
|
} else { |
|
|
filesPreview.style.display = 'none'; |
|
|
convertButton.disabled = true; |
|
|
} |
|
|
} |
|
|
|
|
|
function renderFileCards() { |
|
|
const filesGrid = document.getElementById('files-grid'); |
|
|
|
|
|
filesGrid.innerHTML = selectedFiles.map((file, index) => ` |
|
|
<div class="file-card" id="file-${file.id}"> |
|
|
<button class="remove-button" onclick="removeFile('${file.id}')">Γ</button> |
|
|
<div class="file-preview"> |
|
|
${file.preview ? `<img src="${file.preview}" alt="${file.name}">` : '<span>πΌοΈ</span>'} |
|
|
</div> |
|
|
<div class="file-info"> |
|
|
<h4>${index + 1}. ${file.name}</h4> |
|
|
<p>${file.size}</p> |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
function removeFile(fileId) { |
|
|
selectedFiles = selectedFiles.filter(file => file.id !== fileId); |
|
|
updateUI(); |
|
|
if (selectedFiles.length === 0) { |
|
|
document.getElementById('download-button').style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
function clearAllFiles() { |
|
|
selectedFiles = []; |
|
|
pdfUrl = null; |
|
|
updateUI(); |
|
|
document.getElementById('download-button').style.display = 'none'; |
|
|
document.getElementById('progress-section').style.display = 'none'; |
|
|
} |
|
|
|
|
|
async function convertToPDF() { |
|
|
if (selectedFiles.length === 0) return; |
|
|
|
|
|
const progressSection = document.getElementById('progress-section'); |
|
|
const progressBar = document.getElementById('progress-bar'); |
|
|
const progressText = document.getElementById('progress-text'); |
|
|
const convertButton = document.getElementById('convert-button'); |
|
|
|
|
|
progressSection.style.display = 'block'; |
|
|
convertButton.disabled = true; |
|
|
convertButton.innerHTML = 'π Converting...'; |
|
|
|
|
|
try { |
|
|
|
|
|
const uploadedIds = []; |
|
|
for (let i = 0; i < selectedFiles.length; i++) { |
|
|
const file = selectedFiles[i]; |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('image', file.file); |
|
|
formData.append('id', file.id); |
|
|
|
|
|
await fetch('/pdf/upload', { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
uploadedIds.push(file.id); |
|
|
|
|
|
const progress = Math.round(((i + 1) / selectedFiles.length) * 50); |
|
|
progressBar.style.width = progress + '%'; |
|
|
progressText.textContent = `Uploading: ${progress}%`; |
|
|
} |
|
|
|
|
|
|
|
|
progressText.textContent = 'Converting to PDF...'; |
|
|
progressBar.style.width = '75%'; |
|
|
|
|
|
const convertFormData = new FormData(); |
|
|
convertFormData.append('ids', uploadedIds.join(',')); |
|
|
convertFormData.append('output_name', 'images.pdf'); |
|
|
|
|
|
const response = await fetch('/pdf/convert', { |
|
|
method: 'POST', |
|
|
body: convertFormData |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error('Conversion failed'); |
|
|
} |
|
|
|
|
|
const result = await response.json(); |
|
|
pdfUrl = '/pdf/download?id=' + result.new_filename; |
|
|
|
|
|
progressBar.style.width = '100%'; |
|
|
progressText.textContent = '100% complete'; |
|
|
|
|
|
document.getElementById('download-button').style.display = 'inline-flex'; |
|
|
showMessage('PDF created successfully!', 'success'); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Conversion error:', error); |
|
|
showMessage('Error converting to PDF: ' + error.message, 'error'); |
|
|
} |
|
|
|
|
|
convertButton.disabled = false; |
|
|
convertButton.innerHTML = 'π Convert to PDF'; |
|
|
|
|
|
setTimeout(() => { |
|
|
progressSection.style.display = 'none'; |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
function downloadPDF() { |
|
|
if (pdfUrl) { |
|
|
const link = document.createElement('a'); |
|
|
link.href = pdfUrl; |
|
|
link.download = 'images.pdf'; |
|
|
link.target = '_blank'; |
|
|
document.body.appendChild(link); |
|
|
link.click(); |
|
|
document.body.removeChild(link); |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
|
|
|
</html> |