| <!DOCTYPE html> |
| <html lang="ar" dir="rtl"> |
| <head> |
| <meta charset="UTF-8" /> |
| <title>أداة دمج ملفات المجموعة | PDF بدون تغيير الدقة</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/pdf-lib@1.17.1/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>دمج الملفات الى صيغة PDF</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>مثال: 2026-02-22.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 (كل صورة بصفحة بنفس أبعادها الأصلية)."; |
| } |
| |
| 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 = ""; |
| outputNameInput.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; |
| } |
| |
| 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({ useObjectStreams: true }); |
| |
| |
| 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> |
|
|