CoolFace
Apppublic

Aaeafh/Finalbot

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
pdf_generator.py176 linesDownload Raw Back to root
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