Download index.html from stat2025/PDF: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/stat2025/PDF/resolve/main/index.html
- Command line
-
hf download hf://spaces/stat2025/PDF/index.html
-
curl -L -o index.html https://huggingface.co/spaces/stat2025/PDF/resolve/main/index.html
14.6 kB
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>أداة دمج ملفات المجموعة | PDF بدون تغيير الدقة</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <!-- خط Tajawal --> | |
| <link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="style.css" /> | |
| <!-- مكتبة pdf-lib للدمج داخل المتصفح --> | |
| <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; | |
| // تاريخ اليوم بصيغة YYYY-MM-DD | |
| 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)) { | |
| // ✅ نسخ صفحات PDF كما هي تمامًا | |
| 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> | |