Spaces:
Sleeping
Sleeping
| """ | |
| وحدة توليد الملفات والمخططات | |
| - PDF عبر ReportLab | |
| - Word عبر python-docx | |
| - Excel عبر openpyxl | |
| - مخططات عبر matplotlib | |
| كل الملفات تُحفظ مؤقتاً وتُرسل كمرفقات | |
| """ | |
| import asyncio | |
| import io | |
| import logging | |
| import os | |
| import tempfile | |
| import uuid | |
| from typing import List, Optional, Tuple | |
| logger = logging.getLogger(__name__) | |
| # ضبط matplotlib لـ headless | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import matplotlib.font_manager as fm | |
| # محاولة تسجيل خطوط عربية إن وجدت | |
| try: | |
| for fp in [ | |
| "/usr/share/fonts/truetype/chinese/NotoSansSC-Regular.ttf", | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", | |
| "/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKai-Regular.ttf", | |
| ]: | |
| if os.path.exists(fp): | |
| fm.fontManager.addfont(fp) | |
| plt.rcParams["font.sans-serif"] = ["Noto Sans SC", "DejaVu Sans", "Liberation Sans"] | |
| plt.rcParams["axes.unicode_minus"] = False | |
| except Exception as e: | |
| logger.warning(f"Font setup warning: {e}") | |
| # ===================== PDF ===================== | |
| def create_pdf(content: str, title: str = "مستند") -> str: | |
| """إنشاء PDF من نص - يعيد مسار الملف""" | |
| try: | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer | |
| from reportlab.lib.units import cm | |
| from reportlab.lib.enums import TA_RIGHT | |
| from reportlab.pdfbase import pdfmetrics | |
| from reportlab.pdfbase.ttfonts import TTFont | |
| except ImportError as e: | |
| raise RuntimeError("reportlab غير مثبت") from e | |
| # محاولة تسجيل خط عربي | |
| arabic_font_name = "Helvetica" | |
| try: | |
| arabic_font_path = None | |
| for p in [ | |
| "/usr/share/fonts/truetype/lxgw-wenkai/LXGWWenKai-Regular.ttf", | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", | |
| "/usr/share/fonts/truetype/freefont/FreeSans.ttf", | |
| ]: | |
| if os.path.exists(p): | |
| arabic_font_path = p | |
| break | |
| if arabic_font_path: | |
| pdfmetrics.registerFont(TTFont("ArabicFont", arabic_font_path)) | |
| arabic_font_name = "ArabicFont" | |
| except Exception: | |
| pass | |
| output_path = os.path.join(tempfile.gettempdir(), f"{title[:30]}_{uuid.uuid4().hex[:6]}.pdf") | |
| doc = SimpleDocTemplate( | |
| output_path, pagesize=A4, | |
| leftMargin=2*cm, rightMargin=2*cm, | |
| topMargin=2*cm, bottomMargin=2*cm, | |
| ) | |
| styles = getSampleStyleSheet() | |
| title_style = ParagraphStyle( | |
| "ArabicTitle", parent=styles["Title"], | |
| fontName=arabic_font_name, fontSize=20, alignment=TA_RIGHT, | |
| spaceAfter=20, | |
| ) | |
| body_style = ParagraphStyle( | |
| "ArabicBody", parent=styles["Normal"], | |
| fontName=arabic_font_name, fontSize=12, alignment=TA_RIGHT, | |
| leading=18, spaceAfter=10, | |
| ) | |
| story = [] | |
| story.append(Paragraph(title, title_style)) | |
| story.append(Spacer(1, 1*cm)) | |
| # تقسيم المحتوى لفقرات | |
| for line in content.split("\n"): | |
| if line.strip(): | |
| # escape XML | |
| safe = line.replace("&", "&").replace("<", "<").replace(">", ">") | |
| story.append(Paragraph(safe, body_style)) | |
| else: | |
| story.append(Spacer(1, 0.3*cm)) | |
| doc.build(story) | |
| return output_path | |
| # ===================== DOCX ===================== | |
| def create_docx(content: str, title: str = "مستند") -> str: | |
| """إنشاء Word DOCX من نص""" | |
| try: | |
| from docx import Document | |
| from docx.shared import Pt, Inches, RGBColor | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| except ImportError as e: | |
| raise RuntimeError("python-docx غير مثبت") from e | |
| output_path = os.path.join(tempfile.gettempdir(), f"{title[:30]}_{uuid.uuid4().hex[:6]}.docx") | |
| doc = Document() | |
| # عنوان | |
| title_para = doc.add_heading(title, level=1) | |
| title_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT | |
| # محتوى | |
| for line in content.split("\n"): | |
| if line.strip(): | |
| p = doc.add_paragraph(line) | |
| p.alignment = WD_ALIGN_PARAGRAPH.RIGHT | |
| for run in p.runs: | |
| run.font.size = Pt(12) | |
| doc.save(output_path) | |
| return output_path | |
| # ===================== XLSX ===================== | |
| def create_xlsx(data: List[List], sheet_name: str = "Sheet1", title: str = "بيانات") -> str: | |
| """إنشاء Excel من بيانات (list of lists)""" | |
| try: | |
| from openpyxl import Workbook | |
| from openpyxl.styles import Font, PatternFill, Alignment, Border, Side | |
| from openpyxl.utils import get_column_letter | |
| except ImportError as e: | |
| raise RuntimeError("openpyxl غير مثبت") from e | |
| output_path = os.path.join(tempfile.gettempdir(), f"{title[:30]}_{uuid.uuid4().hex[:6]}.xlsx") | |
| wb = Workbook() | |
| ws = wb.active | |
| ws.title = sheet_name[:31] # حد Excel لاسم الورقة | |
| if not data: | |
| ws.append(["لا توجد بيانات"]) | |
| else: | |
| for row in data: | |
| ws.append(row) | |
| # تنسيق الصف الأول (headers) | |
| if len(data) > 0: | |
| header_font = Font(bold=True, color="FFFFFF", size=11) | |
| header_fill = PatternFill("solid", fgColor="4F81BD") | |
| thin_border = Border( | |
| left=Side(style="thin"), right=Side(style="thin"), | |
| top=Side(style="thin"), bottom=Side(style="thin"), | |
| ) | |
| for cell in ws[1]: | |
| cell.font = header_font | |
| cell.fill = header_fill | |
| cell.alignment = Alignment(horizontal="center", vertical="center") | |
| cell.border = thin_border | |
| # ضبط عرض الأعمدة تلقائياً | |
| for col in ws.columns: | |
| max_len = 0 | |
| col_letter = get_column_letter(col[0].column) | |
| for cell in col: | |
| try: | |
| cell_len = len(str(cell.value)) if cell.value else 0 | |
| if cell_len > max_len: | |
| max_len = cell_len | |
| except Exception: | |
| pass | |
| ws.column_dimensions[col_letter].width = min(max_len + 2, 50) | |
| wb.save(output_path) | |
| return output_path | |
| # ===================== Charts (matplotlib) ===================== | |
| def create_chart( | |
| chart_type: str, | |
| data: dict, | |
| title: str = "مخطط", | |
| xlabel: str = "", | |
| ylabel: str = "", | |
| ) -> str: | |
| """ | |
| إنشاء مخطط بصورة PNG. | |
| chart_type: bar, line, pie, scatter, histogram | |
| data: dict حسب نوع المخطط | |
| - bar/line: {"labels": [...], "values": [...]} | |
| - pie: {"labels": [...], "values": [...]} | |
| - scatter: {"x": [...], "y": [...]} | |
| - histogram: {"values": [...]} | |
| """ | |
| output_path = os.path.join(tempfile.gettempdir(), f"chart_{uuid.uuid4().hex[:6]}.png") | |
| fig, ax = plt.subplots(figsize=(10, 6), constrained_layout=True) | |
| try: | |
| if chart_type == "bar": | |
| labels = data.get("labels", []) | |
| values = data.get("values", []) | |
| ax.bar(labels, values, color="steelblue", edgecolor="navy") | |
| elif chart_type == "line": | |
| labels = data.get("labels", []) | |
| values = data.get("values", []) | |
| ax.plot(labels, values, marker="o", color="crimson", linewidth=2) | |
| ax.grid(True, alpha=0.3) | |
| elif chart_type == "pie": | |
| labels = data.get("labels", []) | |
| values = data.get("values", []) | |
| ax.pie(values, labels=labels, autopct="%1.1f%%", startangle=90) | |
| ax.axis("equal") | |
| elif chart_type == "scatter": | |
| x = data.get("x", []) | |
| y = data.get("y", []) | |
| ax.scatter(x, y, color="darkorange", alpha=0.7, edgecolors="black") | |
| ax.grid(True, alpha=0.3) | |
| elif chart_type == "histogram": | |
| values = data.get("values", []) | |
| ax.hist(values, bins=20, color="mediumpurple", edgecolor="indigo") | |
| else: | |
| raise ValueError(f"نوع مخطط غير معروف: {chart_type}") | |
| if title: | |
| ax.set_title(title, fontsize=14, fontweight="bold") | |
| if xlabel: | |
| ax.set_xlabel(xlabel, fontsize=11) | |
| if ylabel: | |
| ax.set_ylabel(ylabel, fontsize=11) | |
| # تدوير labels المحور السيني إن طالت | |
| if chart_type in ("bar", "line") and data.get("labels"): | |
| if len(data["labels"]) > 5: | |
| plt.setp(ax.get_xticklabels(), rotation=30, ha="right") | |
| fig.savefig(output_path, dpi=120) | |
| finally: | |
| plt.close(fig) | |
| return output_path | |
| # ===================== مساعد ===================== | |
| def cleanup_file(path: str): | |
| """حذف ملف مؤقت بأمان""" | |
| try: | |
| if path and os.path.exists(path): | |
| os.remove(path) | |
| except Exception as e: | |
| logger.warning(f"Could not delete {path}: {e}") | |
| async def run_in_executor(func, *args, **kwargs): | |
| """تشغيل دالة متزامنة في thread pool""" | |
| return await asyncio.to_thread(func, *args, **kwargs) | |