Aaeafh/Finalbot
0
1import os2import io3import arabic_reshaper4from bidi.algorithm import get_display5from reportlab.lib.pagesizes import A46from reportlab.lib.styles import ParagraphStyle7from reportlab.lib.units import cm8from reportlab.lib import colors9from reportlab.platypus import (10 SimpleDocTemplate, Paragraph, Spacer, HRFlowable,11 Image as RLImage, KeepTogether,12)13from reportlab.pdfbase import pdfmetrics14from reportlab.pdfbase.ttfonts import TTFont15 16# ─── Font Setup ──────────────────────────────────────────────────────────────17 18FONTS_DIR = os.path.join(os.path.dirname(__file__), "fonts")19FONT_NAME = "Arabic"20 21def _register_arabic_font():22 candidates = [23 os.path.join(FONTS_DIR, "NotoSansArabic-Regular.ttf"),24 os.path.join(FONTS_DIR, "Amiri-Regular.ttf"),25 "/usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf",26 ]27 for path in candidates:28 if os.path.exists(path):29 try:30 pdfmetrics.registerFont(TTFont(FONT_NAME, path))31 return True32 except Exception:33 pass34 try:35 import urllib.request36 os.makedirs(FONTS_DIR, exist_ok=True)37 dest = os.path.join(FONTS_DIR, "NotoSansArabic-Regular.ttf")38 urllib.request.urlretrieve(39 "https://github.com/googlefonts/noto-fonts/raw/main/hinted/ttf/NotoSansArabic/NotoSansArabic-Regular.ttf",40 dest,41 )42 pdfmetrics.registerFont(TTFont(FONT_NAME, dest))43 return True44 except Exception:45 return False46 47_font_ready = _register_arabic_font()48_FONT = FONT_NAME if _font_ready else "Helvetica"49 50 51# ─── Helpers ─────────────────────────────────────────────────────────────────52 53def ar(text: str) -> str:54 if not text:55 return ""56 reshaped = arabic_reshaper.reshape(text)57 return get_display(reshaped)58 59 60def _style(size=11, bold=False, center=False, color=colors.black):61 return ParagraphStyle(62 name=f"s_{size}_{bold}_{center}_{color}",63 fontName=_FONT,64 fontSize=size,65 leading=size * 1.65,66 alignment=1 if center else 2,67 wordWrap="RTL",68 textColor=color,69 spaceAfter=2,70 )71 72 73def _img(image_bytes: bytes, max_width=13 * cm, max_height=9 * cm):74 buf = io.BytesIO(image_bytes)75 img = RLImage(buf)76 w, h = img.imageWidth, img.imageHeight77 ratio = min(max_width / w, max_height / h, 1.0)78 img.drawWidth = w * ratio79 img.drawHeight = h * ratio80 return img81 82 83# ─── Main Generator ──────────────────────────────────────────────────────────84 85async def generate_quiz_pdf(section_name: str, questions: list, bot=None) -> bytes:86 """87 Generate a compact, clean PDF matching the target format:88 السؤال (N): نص السؤال89 [صورة اختيارية]90 الجواب الصحيح: نص الجواب91 [صورة اختيارية]92 ──────────────── (فاصل خفيف بين الأسئلة)93 """94 buf = io.BytesIO()95 doc = SimpleDocTemplate(96 buf,97 pagesize=A4,98 rightMargin=2.2 * cm,99 leftMargin=2.2 * cm,100 topMargin=2 * cm,101 bottomMargin=2 * cm,102 )103 104 story = []105 106 # ── Title ────────────────────────────────────────────────────────────────107 story.append(Paragraph(108 ar(f"تجميع كويزات مادة {section_name}"),109 _style(size=15, bold=True, center=True),110 ))111 story.append(Spacer(1, 0.3 * cm))112 story.append(HRFlowable(width="100%", thickness=1.2, color=colors.black))113 story.append(Spacer(1, 0.35 * cm))114 115 # ── Questions ─────────────────────────────────────────────────────────────116 for idx, q in enumerate(questions):117 num = q["quiz_num"]118 block = [] # نجمع كل عناصر السؤال في block واحد لـ KeepTogether119 120 # ── السؤال ──121 q_text = q["question_text"] or ""122 if q_text:123 # نص السؤال ورقمه في سطر واحد124 label_and_text = ar(f"السؤال ({num}): {q_text}")125 block.append(Paragraph(label_and_text, _style(size=11)))126 else:127 # بدون نص — اعرض الرقم فقط ثم الصورة128 block.append(Paragraph(ar(f"السؤال ({num}):"), _style(size=11, bold=True)))129 130 # صورة السؤال (إن وجدت)131 if q["question_image"] and bot:132 try:133 file = await bot.get_file(q["question_image"])134 img_bytes = await file.download_as_bytearray()135 block.append(Spacer(1, 0.15 * cm))136 block.append(_img(bytes(img_bytes)))137 block.append(Spacer(1, 0.1 * cm))138 except Exception:139 block.append(Paragraph(ar("[صورة غير متاحة]"), _style(size=9, color=colors.grey)))140 141 # ── الجواب ──142 a_text = q["answer_text"] or ""143 if a_text:144 label_and_ans = ar(f"الجواب الصحيح: {a_text}")145 block.append(Paragraph(label_and_ans, _style(size=11)))146 else:147 block.append(Paragraph(ar("الجواب الصحيح:"), _style(size=11, bold=True)))148 149 # صورة الجواب (إن وجدت)150 if q["answer_image"] and bot:151 try:152 file = await bot.get_file(q["answer_image"])153 img_bytes = await file.download_as_bytearray()154 block.append(Spacer(1, 0.15 * cm))155 block.append(_img(bytes(img_bytes)))156 block.append(Spacer(1, 0.1 * cm))157 except Exception:158 block.append(Paragraph(ar("[صورة غير متاحة]"), _style(size=9, color=colors.grey)))159 160 story.append(KeepTogether(block))161 162 # فاصل خفيف بين الأسئلة (ليس بعد الأخير)163 if idx < len(questions) - 1:164 story.append(Spacer(1, 0.25 * cm))165 story.append(HRFlowable(166 width="100%",167 thickness=0.4,168 color=colors.Color(0.7, 0.7, 0.7),169 lineCap="round",170 dash=[3, 4], # خط منقّط171 ))172 story.append(Spacer(1, 0.2 * cm))173 174 doc.build(story)175 return buf.getvalue()176 