|
|
<!DOCTYPE html> |
|
|
<html lang="ar" dir="rtl"> |
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<title>أدمـج ملـفاتك بسهولـة</title> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
|
|
|
|
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
|
<link rel="stylesheet" href="style.css" /> |
|
|
|
|
|
<script src="https://unpkg.com/[email protected]/dist/pdf-lib.min.js"></script> |
|
|
</head> |
|
|
<body> |
|
|
<div class="page"> |
|
|
|
|
|
|
|
|
<header class="topbar"> |
|
|
<span class="credit">تصميم وإعداد الدعم الفني: نوف الناصر</span> |
|
|
</header> |
|
|
|
|
|
|
|
|
<main class="main"> |
|
|
|
|
|
|
|
|
<section class="hero"> |
|
|
<div class="logo-mark">PDF</div> |
|
|
<h1>أدمـج ملـفاتك بسهولـة</h1> |
|
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
<section class="steps"> |
|
|
<div class="step"> |
|
|
<span class="step-number">1</span> |
|
|
<span class="step-text">اختر الملفات (يمكن الإضافة على دفعات من الجوال أو الكمبيوتر).</span> |
|
|
</div> |
|
|
<div class="step"> |
|
|
<span class="step-number">2</span> |
|
|
<span class="step-text">رتّب الملفات باستخدام الأسهم أو احذف ما لا تحتاجه.</span> |
|
|
</div> |
|
|
<div class="step"> |
|
|
<span class="step-number">3</span> |
|
|
<span class="step-text">اضغط دمج، وانتظر حتى يكتمل الشريط لتحميل ملف PDF النهائي.</span> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section class="card main-card"> |
|
|
|
|
|
|
|
|
<div class="card-section card-select"> |
|
|
<h2 class="card-title"> اختيار الملفات المراد دمجها بي دي اف او صور او الاثنين معاً</h2> |
|
|
|
|
|
<label class="file-picker"> |
|
|
<span class="file-picker-icon">📂</span> |
|
|
<span class="file-picker-text">اضغط لاختيار الملفات (يمكن التكرار والإضافة لاحقًا)</span> |
|
|
<input id="files" type="file" multiple accept=".pdf,image/*" /> |
|
|
</label> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="card-section"> |
|
|
<div id="fileList" class="file-list hidden"></div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="card-section card-output"> |
|
|
<div class="card-row inline"> |
|
|
<label for="outputName" class="card-label"> |
|
|
اسم ملف الإخراج (اختياري) |
|
|
<span class="label-note"> |
|
|
في حال تركه فارغًا سيتم استخدام تاريخ اليوم تلقائيًا، |
|
|
<strong>مثال: 2025-11-12.pdf</strong> |
|
|
</span> |
|
|
</label> |
|
|
<input |
|
|
id="outputName" |
|
|
type="text" |
|
|
class="output-input" |
|
|
placeholder="اكتب اسم الملف هنا أو اتركه فارغًا لاستخدام تاريخ اليوم" |
|
|
/> |
|
|
</div> |
|
|
|
|
|
<div class="actions"> |
|
|
<button id="mergeBtn" class="btn-main">دمج وإنشاء ملف PDF واحد</button> |
|
|
<button id="clearBtn" class="btn-secondary" type="button">مسح جميع الملفات</button> |
|
|
</div> |
|
|
|
|
|
<div id="status" class="status"></div> |
|
|
|
|
|
<div id="progress" class="progress hidden"> |
|
|
<div id="progressText" class="progress-text">جاري المعالجة...</div> |
|
|
<div class="progress-bar"> |
|
|
<div id="progressFill" class="progress-fill"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</section> |
|
|
</main> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
const filesInput = document.getElementById("files"); |
|
|
const mergeBtn = document.getElementById("mergeBtn"); |
|
|
const clearBtn = document.getElementById("clearBtn"); |
|
|
const statusDiv = document.getElementById("status"); |
|
|
const fileListDiv = document.getElementById("fileList"); |
|
|
const outputNameInput = document.getElementById("outputName"); |
|
|
const progressDiv = document.getElementById("progress"); |
|
|
const progressText = document.getElementById("progressText"); |
|
|
const progressFill = document.getElementById("progressFill"); |
|
|
|
|
|
let selectedFiles = []; |
|
|
const MAX_RECOMMENDED_FILES = 200; |
|
|
|
|
|
|
|
|
function getTodayDateString() { |
|
|
const d = new Date(); |
|
|
const year = d.getFullYear(); |
|
|
const month = String(d.getMonth() + 1).padStart(2, "0"); |
|
|
const day = String(d.getDate()).padStart(2, "0"); |
|
|
return `${year}-${month}-${day}`; |
|
|
} |
|
|
|
|
|
function setStatus(msg, type = "") { |
|
|
statusDiv.textContent = msg; |
|
|
statusDiv.className = "status" + (type ? " " + type : ""); |
|
|
if (!msg) statusDiv.className = "status"; |
|
|
} |
|
|
|
|
|
function showProgress(show) { |
|
|
if (show) { |
|
|
progressDiv.classList.remove("hidden"); |
|
|
progressFill.style.width = "0%"; |
|
|
progressText.textContent = "جاري المعالجة..."; |
|
|
} else { |
|
|
progressDiv.classList.add("hidden"); |
|
|
} |
|
|
} |
|
|
|
|
|
function setProgress(current, total, label = "معالجة الملفات") { |
|
|
if (!total || total < 1) total = 1; |
|
|
const percent = Math.floor((current / total) * 100); |
|
|
progressFill.style.width = percent + "%"; |
|
|
progressText.textContent = `${label} (${current} من ${total}) - ${percent}%`; |
|
|
} |
|
|
|
|
|
function isImage(file) { |
|
|
const name = file.name.toLowerCase(); |
|
|
return ( |
|
|
file.type.startsWith("image/") || |
|
|
name.endsWith(".jpg") || |
|
|
name.endsWith(".jpeg") || |
|
|
name.endsWith(".png") |
|
|
); |
|
|
} |
|
|
|
|
|
function isPDF(file) { |
|
|
const name = file.name.toLowerCase(); |
|
|
return ( |
|
|
file.type === "application/pdf" || |
|
|
name.endsWith(".pdf") |
|
|
); |
|
|
} |
|
|
|
|
|
function getFilesInfo(files) { |
|
|
let hasImages = false; |
|
|
let hasPDFs = false; |
|
|
files.forEach((f) => { |
|
|
if (isImage(f)) hasImages = true; |
|
|
else if (isPDF(f)) hasPDFs = true; |
|
|
}); |
|
|
return { hasImages, hasPDFs }; |
|
|
} |
|
|
|
|
|
function renderFileList(files) { |
|
|
if (!files.length) { |
|
|
fileListDiv.classList.add("hidden"); |
|
|
fileListDiv.innerHTML = ""; |
|
|
return; |
|
|
} |
|
|
|
|
|
fileListDiv.classList.remove("hidden"); |
|
|
const { hasImages, hasPDFs } = getFilesInfo(files); |
|
|
let modeText = ""; |
|
|
if (hasImages && hasPDFs) { |
|
|
modeText = "الوضع الحالي: دمج صور + ملفات PDF في ملف واحد، مع الحفاظ على المقاس الأصلي لكل صفحة."; |
|
|
} else if (hasPDFs) { |
|
|
modeText = "الوضع الحالي: دمج ملفات PDF في ملف واحد (نسخ الصفحات كما هي)."; |
|
|
} else if (hasImages) { |
|
|
modeText = "الوضع الحالي: تحويل صور إلى PDF مع إنشاء صفحة بكل صورة وبنفس أبعادها الأصلية."; |
|
|
} else { |
|
|
modeText = "لا توجد ملفات مدعومة في القائمة."; |
|
|
} |
|
|
|
|
|
fileListDiv.innerHTML = ` |
|
|
<div class="file-list-header"> |
|
|
<span>الملفات المختارة: ${files.length}</span> |
|
|
<span class="file-note">يمكنك إعادة الترتيب أو حذف أي ملف قبل الدمج.</span> |
|
|
</div> |
|
|
<div class="mode-label">${modeText}</div> |
|
|
<ul class="file-list-ul"> |
|
|
${files |
|
|
.map( |
|
|
(f, i) => ` |
|
|
<li> |
|
|
<span class="index">${i + 1}</span> |
|
|
<span class="name" title="${f.name}">${f.name}</span> |
|
|
<span class="size">${(f.size / 1024).toFixed(1)} كيلوبايت</span> |
|
|
<div class="row-actions"> |
|
|
<button class="move-btn" data-index="${i}" data-dir="up" title="نقل لأعلى">↑</button> |
|
|
<button class="move-btn" data-index="${i}" data-dir="down" title="نقل لأسفل">↓</button> |
|
|
<button class="delete-btn" data-index="${i}" title="حذف الملف">×</button> |
|
|
</div> |
|
|
</li>` |
|
|
) |
|
|
.join("")} |
|
|
</ul> |
|
|
`; |
|
|
|
|
|
if (files.length > MAX_RECOMMENDED_FILES) { |
|
|
setStatus( |
|
|
"تنبيه: عدد الملفات كبير، قد تستغرق عملية الدمج وقتًا أطول على بعض الأجهزة.", |
|
|
"warning" |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
fileListDiv.querySelectorAll(".delete-btn").forEach((btn) => { |
|
|
btn.addEventListener("click", (e) => { |
|
|
const index = parseInt(e.currentTarget.dataset.index, 10); |
|
|
if (!isNaN(index)) { |
|
|
selectedFiles.splice(index, 1); |
|
|
renderFileList(selectedFiles); |
|
|
if (!selectedFiles.length) { |
|
|
setStatus(""); |
|
|
showProgress(false); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
fileListDiv.querySelectorAll(".move-btn").forEach((btn) => { |
|
|
btn.addEventListener("click", (e) => { |
|
|
const index = parseInt(e.currentTarget.dataset.index, 10); |
|
|
const dir = e.currentTarget.dataset.dir; |
|
|
if (isNaN(index)) return; |
|
|
|
|
|
if (dir === "up" && index > 0) { |
|
|
[selectedFiles[index - 1], selectedFiles[index]] = |
|
|
[selectedFiles[index], selectedFiles[index - 1]]; |
|
|
} else if (dir === "down" && index < selectedFiles.length - 1) { |
|
|
[selectedFiles[index + 1], selectedFiles[index]] = |
|
|
[selectedFiles[index], selectedFiles[index + 1]]; |
|
|
} |
|
|
renderFileList(selectedFiles); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function downloadPdf(bytes, filename) { |
|
|
const blob = new Blob([bytes], { type: "application/pdf" }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement("a"); |
|
|
a.href = url; |
|
|
a.download = filename; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
a.remove(); |
|
|
URL.revokeObjectURL(url); |
|
|
} |
|
|
|
|
|
|
|
|
filesInput.addEventListener("change", () => { |
|
|
const newFilesRaw = Array.from(filesInput.files || []); |
|
|
if (!newFilesRaw.length) return; |
|
|
|
|
|
const newFiles = newFilesRaw.filter((f) => isImage(f) || isPDF(f)); |
|
|
const map = new Map(); |
|
|
|
|
|
[...selectedFiles, ...newFiles].forEach((f) => { |
|
|
const key = `${f.name}|${f.size}|${f.lastModified}`; |
|
|
if (!map.has(key)) map.set(key, f); |
|
|
}); |
|
|
|
|
|
selectedFiles = Array.from(map.values()); |
|
|
renderFileList(selectedFiles); |
|
|
setStatus(""); |
|
|
filesInput.value = ""; |
|
|
}); |
|
|
|
|
|
|
|
|
clearBtn.addEventListener("click", () => { |
|
|
selectedFiles = []; |
|
|
renderFileList([]); |
|
|
setStatus("تم مسح جميع الملفات المختارة.", "ok"); |
|
|
filesInput.value = ""; |
|
|
showProgress(false); |
|
|
}); |
|
|
|
|
|
|
|
|
mergeBtn.addEventListener("click", async () => { |
|
|
const files = [...selectedFiles]; |
|
|
|
|
|
if (!files.length) { |
|
|
setStatus("الرجاء اختيار الملفات أولاً.", "error"); |
|
|
return; |
|
|
} |
|
|
|
|
|
const unsupported = files.filter((f) => !isImage(f) && !isPDF(f)); |
|
|
if (unsupported.length) { |
|
|
setStatus("يوجد ملفات غير مدعومة. يرجى حذفها من القائمة.", "error"); |
|
|
return; |
|
|
} |
|
|
|
|
|
const { hasImages, hasPDFs } = getFilesInfo(files); |
|
|
if (!hasImages && !hasPDFs) { |
|
|
setStatus("لا توجد ملفات مدعومة للدمج.", "error"); |
|
|
return; |
|
|
} |
|
|
|
|
|
renderFileList(files); |
|
|
|
|
|
try { |
|
|
setStatus("جاري معالجة الملفات...", "loading"); |
|
|
showProgress(true); |
|
|
mergeBtn.disabled = true; |
|
|
mergeBtn.classList.add("disabled"); |
|
|
filesInput.disabled = true; |
|
|
clearBtn.disabled = true; |
|
|
|
|
|
const pdfDoc = await PDFLib.PDFDocument.create(); |
|
|
const totalSteps = files.length; |
|
|
let currentStep = 0; |
|
|
|
|
|
for (const file of files) { |
|
|
const bytes = await file.arrayBuffer(); |
|
|
|
|
|
if (isPDF(file)) { |
|
|
|
|
|
const donorPdf = await PDFLib.PDFDocument.load(bytes); |
|
|
const pages = await pdfDoc.copyPages( |
|
|
donorPdf, |
|
|
donorPdf.getPageIndices() |
|
|
); |
|
|
pages.forEach((p) => pdfDoc.addPage(p)); |
|
|
} else if (isImage(file)) { |
|
|
|
|
|
const lower = file.name.toLowerCase(); |
|
|
let image; |
|
|
if ( |
|
|
file.type === "image/jpeg" || |
|
|
file.type === "image/jpg" || |
|
|
lower.endsWith(".jpg") || |
|
|
lower.endsWith(".jpeg") |
|
|
) { |
|
|
image = await pdfDoc.embedJpg(bytes); |
|
|
} else { |
|
|
image = await pdfDoc.embedPng(bytes); |
|
|
} |
|
|
|
|
|
const imgW = image.width; |
|
|
const imgH = image.height; |
|
|
|
|
|
|
|
|
const page = pdfDoc.addPage([imgW, imgH]); |
|
|
page.drawImage(image, { x: 0, y: 0, width: imgW, height: imgH }); |
|
|
} |
|
|
|
|
|
currentStep += 1; |
|
|
setProgress(currentStep, totalSteps); |
|
|
} |
|
|
|
|
|
const pdfBytes = await pdfDoc.save(); |
|
|
|
|
|
|
|
|
const defaultName = getTodayDateString() + ".pdf"; |
|
|
const outName = |
|
|
(outputNameInput.value || defaultName).trim() || defaultName; |
|
|
|
|
|
downloadPdf(pdfBytes, outName); |
|
|
setStatus("تم إنشاء ملف PDF النهائي بنجاح.", "ok"); |
|
|
showProgress(false); |
|
|
|
|
|
selectedFiles = []; |
|
|
renderFileList([]); |
|
|
filesInput.value = ""; |
|
|
} catch (err) { |
|
|
console.error(err); |
|
|
setStatus("حدث خطأ أثناء المعالجة. تأكد من الملفات وحاول مرة أخرى.", "error"); |
|
|
showProgress(false); |
|
|
} finally { |
|
|
mergeBtn.disabled = false; |
|
|
mergeBtn.classList.remove("disabled"); |
|
|
filesInput.disabled = false; |
|
|
clearBtn.disabled = false; |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|