تم الانتهاء من آخر دفعة تحسينات على المشروع، وتشمل:

تحويل لوحة الإدارة الداخلية من secret header إلى session auth حقيقي مع صلاحيات admin.
إضافة دعم إدارة الأدوار من داخل لوحة الإدارة نفسها، مع حماية الحسابات المعتمدة عبر INTERNAL_ADMIN_EMAILS.
تحسين بيانات المستخدم في الواجهة والباكند لتشمل role وis_allowlisted_admin.
إضافة اختبار frontend مخصص لصفحة /internal/admin بدل الاعتماد فقط على build واختبار routes.
تحسين إضافي في الأداء عبر إزالة الاعتماد على pdfjs-dist/pdf.worker في عدّ صفحات PDF واستبداله بمسار أخف باستخدام pdf-lib.
تحسين تقسيم الـ chunks في build لتقليل أثر الحزم الكبيرة وفصل أجزاء مثل network, icons, pdf-core, وeditor.
التحقق الذي تم:

نجاح build للواجهة.
نجاح اختبار صفحة الإدارة الداخلية في frontend.
نجاح اختبارات auth/admin في backend.
نجاح full backend suite مسبقًا مع EXIT:0.
ولو تريد نسخة أقصر جدًا، استخدم هذه:

آخر التحديثات:
تم تحسين نظام الإدارة الداخلية ليعتمد على صلاحيات وجلسات حقيقية بدل secret header، مع إضافة إدارة أدوار من لوحة admin نفسها، وإضافة اختبارات frontend مخصصة للوحة، وتحسين أداء الواجهة عبر إزالة pdf.worker وتحسين تقسيم الـ chunks في build. جميع الاختبارات والتحققات الأساسية المطلوبة نجح
This commit is contained in:
Your Name
2026-03-16 13:50:45 +02:00
parent b5d97324a9
commit 957d37838c
85 changed files with 9915 additions and 119 deletions

View File

@@ -0,0 +1,278 @@
"""PDF conversion service — PDF↔PowerPoint, Excel→PDF, PowerPoint→PDF, Sign PDF."""
import os
import io
import logging
import subprocess
import tempfile
logger = logging.getLogger(__name__)
class PDFConvertError(Exception):
"""Custom exception for PDF conversion failures."""
pass
# ---------------------------------------------------------------------------
# PDF to PowerPoint (PPTX)
# ---------------------------------------------------------------------------
def pdf_to_pptx(input_path: str, output_path: str) -> dict:
"""Convert a PDF to PowerPoint by rendering each page as a slide image.
Args:
input_path: Path to the input PDF
output_path: Path for the output PPTX
Returns:
dict with total_slides and output_size
Raises:
PDFConvertError: If conversion fails
"""
try:
from pdf2image import convert_from_path
from pptx import Presentation
from pptx.util import Inches, Emu
images = convert_from_path(input_path, dpi=200)
if not images:
raise PDFConvertError("PDF has no pages or could not be rendered.")
prs = Presentation()
# Use widescreen 16:9 layout
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
for img in images:
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank layout
img_stream = io.BytesIO()
img.save(img_stream, format="PNG")
img_stream.seek(0)
# Scale image to fill slide
img_w, img_h = img.size
slide_w = prs.slide_width
slide_h = prs.slide_height
ratio = min(slide_w / Emu(int(img_w * 914400 / 200)),
slide_h / Emu(int(img_h * 914400 / 200)))
pic_w = int(img_w * 914400 / 200 * ratio)
pic_h = int(img_h * 914400 / 200 * ratio)
left = (slide_w - pic_w) // 2
top = (slide_h - pic_h) // 2
slide.shapes.add_picture(img_stream, left, top, pic_w, pic_h)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
prs.save(output_path)
output_size = os.path.getsize(output_path)
logger.info(f"PDF→PPTX: {len(images)} slides ({output_size} bytes)")
return {"total_slides": len(images), "output_size": output_size}
except PDFConvertError:
raise
except Exception as e:
raise PDFConvertError(f"PDF to PowerPoint conversion failed: {str(e)}")
# ---------------------------------------------------------------------------
# Excel (XLSX) to PDF
# ---------------------------------------------------------------------------
def excel_to_pdf(input_path: str, output_dir: str) -> str:
"""Convert an Excel file to PDF using LibreOffice headless.
Args:
input_path: Path to the input XLSX/XLS file
output_dir: Directory for the output file
Returns:
Path to the converted PDF
Raises:
PDFConvertError: If conversion fails
"""
os.makedirs(output_dir, exist_ok=True)
user_install_dir = tempfile.mkdtemp(prefix="lo_excel2pdf_")
cmd = [
"soffice",
"--headless",
"--norestore",
f"-env:UserInstallation=file://{user_install_dir}",
"--convert-to", "pdf",
"--outdir", output_dir,
input_path,
]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=120,
env={**os.environ, "HOME": user_install_dir},
)
input_basename = os.path.splitext(os.path.basename(input_path))[0]
output_path = os.path.join(output_dir, f"{input_basename}.pdf")
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
logger.info(f"Excel→PDF conversion successful: {output_path}")
return output_path
if result.returncode != 0:
stderr = result.stderr or ""
real_errors = [
line for line in stderr.strip().splitlines()
if not line.startswith("Warning: failed to launch javaldx")
]
error_msg = "\n".join(real_errors) if real_errors else stderr
raise PDFConvertError(f"Conversion failed: {error_msg or 'Unknown error'}")
raise PDFConvertError("Output file was not created.")
except subprocess.TimeoutExpired:
raise PDFConvertError("Conversion timed out. File may be too large.")
except FileNotFoundError:
raise PDFConvertError("LibreOffice is not installed on the server.")
finally:
import shutil
shutil.rmtree(user_install_dir, ignore_errors=True)
# ---------------------------------------------------------------------------
# PowerPoint (PPTX) to PDF
# ---------------------------------------------------------------------------
def pptx_to_pdf(input_path: str, output_dir: str) -> str:
"""Convert a PowerPoint file to PDF using LibreOffice headless.
Args:
input_path: Path to the input PPTX/PPT file
output_dir: Directory for the output file
Returns:
Path to the converted PDF
Raises:
PDFConvertError: If conversion fails
"""
os.makedirs(output_dir, exist_ok=True)
user_install_dir = tempfile.mkdtemp(prefix="lo_pptx2pdf_")
cmd = [
"soffice",
"--headless",
"--norestore",
f"-env:UserInstallation=file://{user_install_dir}",
"--convert-to", "pdf",
"--outdir", output_dir,
input_path,
]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=120,
env={**os.environ, "HOME": user_install_dir},
)
input_basename = os.path.splitext(os.path.basename(input_path))[0]
output_path = os.path.join(output_dir, f"{input_basename}.pdf")
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
logger.info(f"PPTX→PDF conversion successful: {output_path}")
return output_path
if result.returncode != 0:
stderr = result.stderr or ""
real_errors = [
line for line in stderr.strip().splitlines()
if not line.startswith("Warning: failed to launch javaldx")
]
error_msg = "\n".join(real_errors) if real_errors else stderr
raise PDFConvertError(f"Conversion failed: {error_msg or 'Unknown error'}")
raise PDFConvertError("Output file was not created.")
except subprocess.TimeoutExpired:
raise PDFConvertError("Conversion timed out. File may be too large.")
except FileNotFoundError:
raise PDFConvertError("LibreOffice is not installed on the server.")
finally:
import shutil
shutil.rmtree(user_install_dir, ignore_errors=True)
# ---------------------------------------------------------------------------
# Sign PDF (overlay signature image on a page)
# ---------------------------------------------------------------------------
def sign_pdf(
input_path: str,
signature_path: str,
output_path: str,
page: int = 0,
x: float = 100,
y: float = 100,
width: float = 200,
height: float = 80,
) -> dict:
"""Overlay a signature image onto a PDF page.
Args:
input_path: Path to the input PDF
signature_path: Path to the signature image (PNG with transparency)
output_path: Path for the signed output PDF
page: 0-based page index to place signature
x: X coordinate (points from left)
y: Y coordinate (points from bottom)
width: Signature width in points
height: Signature height in points
Returns:
dict with total_pages and output_size
Raises:
PDFConvertError: If signing fails
"""
try:
from PyPDF2 import PdfReader, PdfWriter
from reportlab.pdfgen import canvas as rl_canvas
from reportlab.lib.utils import ImageReader
reader = PdfReader(input_path)
total_pages = len(reader.pages)
if total_pages == 0:
raise PDFConvertError("PDF has no pages.")
if page < 0 or page >= total_pages:
raise PDFConvertError(f"Page {page + 1} does not exist (PDF has {total_pages} pages).")
target_page = reader.pages[page]
page_box = target_page.mediabox
page_width = float(page_box.width)
page_height = float(page_box.height)
# Create overlay PDF with the signature image
overlay_stream = io.BytesIO()
c = rl_canvas.Canvas(overlay_stream, pagesize=(page_width, page_height))
sig_img = ImageReader(signature_path)
c.drawImage(sig_img, x, y, width=width, height=height, mask="auto")
c.save()
overlay_stream.seek(0)
overlay_reader = PdfReader(overlay_stream)
overlay_page = overlay_reader.pages[0]
writer = PdfWriter()
for i, pg in enumerate(reader.pages):
if i == page:
pg.merge_page(overlay_page)
writer.add_page(pg)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with open(output_path, "wb") as f:
writer.write(f)
output_size = os.path.getsize(output_path)
logger.info(f"Sign PDF: signature on page {page + 1} ({output_size} bytes)")
return {"total_pages": total_pages, "output_size": output_size, "signed_page": page + 1}
except PDFConvertError:
raise
except Exception as e:
raise PDFConvertError(f"Failed to sign PDF: {str(e)}")