Spaces:
Running
Running
| from reportlab.platypus import ( | |
| SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, | |
| HRFlowable, KeepTogether | |
| ) | |
| from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame | |
| from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet | |
| from reportlab.lib import colors | |
| from reportlab.lib.units import inch, mm | |
| from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.pdfgen import canvas | |
| from datetime import datetime | |
| import os | |
| # ββ Palette ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DARK_BG = colors.HexColor("#0D1117") | |
| PANEL_BG = colors.HexColor("#161B22") | |
| BORDER_COLOR = colors.HexColor("#30363D") | |
| ACCENT_CYAN = colors.HexColor("#22D3EE") | |
| ACCENT_PINK = colors.HexColor("#EC4899") | |
| TEXT_PRIMARY = colors.HexColor("#E6EDF3") | |
| TEXT_MUTED = colors.HexColor("#8B949E") | |
| TEXT_DIM = colors.HexColor("#484F58") | |
| RISK_HIGH_BG = colors.HexColor("#3D1515") | |
| RISK_HIGH_FG = colors.HexColor("#F87171") | |
| RISK_HIGH_BAR = colors.HexColor("#EF4444") | |
| RISK_MED_BG = colors.HexColor("#2D2008") | |
| RISK_MED_FG = colors.HexColor("#FBBF24") | |
| RISK_MED_BAR = colors.HexColor("#F59E0B") | |
| RISK_LOW_BG = colors.HexColor("#0D2818") | |
| RISK_LOW_FG = colors.HexColor("#4ADE80") | |
| RISK_LOW_BAR = colors.HexColor("#22C55E") | |
| WHITE = colors.white | |
| BLACK = colors.black | |
| # ββ Page canvas callback (header + footer on every page) βββββββββββββββββββββ | |
| def _draw_page(canvas_obj, doc, report_id, timestamp): | |
| W, H = A4 | |
| canvas_obj.saveState() | |
| # ββ Top header bar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| canvas_obj.setFillColor(DARK_BG) | |
| canvas_obj.rect(0, H - 58, W, 58, fill=1, stroke=0) | |
| # Thin cyan accent line under header | |
| canvas_obj.setFillColor(ACCENT_CYAN) | |
| canvas_obj.rect(0, H - 60, W, 2, fill=1, stroke=0) | |
| # Logo / product name | |
| canvas_obj.setFillColor(WHITE) | |
| canvas_obj.setFont("Helvetica-Bold", 16) | |
| canvas_obj.drawString(40, H - 36, "SENTINEL") | |
| canvas_obj.setFillColor(ACCENT_CYAN) | |
| canvas_obj.setFont("Helvetica-Bold", 16) | |
| canvas_obj.drawString(40 + canvas_obj.stringWidth("SENTINEL", "Helvetica-Bold", 16) + 3, H - 36, "AI") | |
| # Sub-label | |
| canvas_obj.setFillColor(TEXT_MUTED) | |
| canvas_obj.setFont("Helvetica", 7) | |
| canvas_obj.drawString(40, H - 48, "STRUCTURED INTELLIGENCE BRIEF Β· THREAT ANALYSIS DIVISION") | |
| # Report ID (right side) | |
| canvas_obj.setFillColor(TEXT_MUTED) | |
| canvas_obj.setFont("Helvetica", 7) | |
| id_text = f"REPORT ID: {report_id}" | |
| canvas_obj.drawRightString(W - 40, H - 32, id_text) | |
| canvas_obj.drawRightString(W - 40, H - 44, f"GENERATED: {timestamp}") | |
| # ββ Bottom footer bar βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| canvas_obj.setFillColor(DARK_BG) | |
| canvas_obj.rect(0, 0, W, 36, fill=1, stroke=0) | |
| # Thin line above footer | |
| canvas_obj.setFillColor(BORDER_COLOR) | |
| canvas_obj.rect(0, 36, W, 1, fill=1, stroke=0) | |
| canvas_obj.setFillColor(TEXT_DIM) | |
| canvas_obj.setFont("Helvetica", 7) | |
| canvas_obj.drawString(40, 14, "CONFIDENTIAL β FOR AUTHORIZED USE ONLY Β· SentinelAI Β© 2026") | |
| canvas_obj.drawRightString(W - 40, 14, f"Page {doc.page}") | |
| canvas_obj.restoreState() | |
| # ββ Style factory βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _styles(): | |
| base = getSampleStyleSheet() | |
| def ps(name, **kw): | |
| defaults = dict(fontName="Helvetica", fontSize=9, leading=13, | |
| textColor=TEXT_PRIMARY, spaceAfter=0, spaceBefore=0) | |
| defaults.update(kw) | |
| return ParagraphStyle(name, **defaults) | |
| return { | |
| "section_label": ps("sl", | |
| fontName="Helvetica-Bold", fontSize=7, textColor=ACCENT_CYAN, | |
| spaceBefore=18, spaceAfter=4, leading=9, | |
| ), | |
| "section_rule": ps("sr"), # placeholder, we use HRFlowable | |
| "body": ps("body", fontSize=9, leading=14, textColor=TEXT_PRIMARY), | |
| "body_muted": ps("bm", fontSize=8, leading=12, textColor=TEXT_MUTED), | |
| "risk_score": ps("rs", | |
| fontName="Helvetica-Bold", fontSize=36, leading=40, | |
| textColor=WHITE, alignment=TA_CENTER, | |
| ), | |
| "risk_label": ps("rl", | |
| fontName="Helvetica-Bold", fontSize=11, leading=14, | |
| textColor=WHITE, alignment=TA_CENTER, | |
| ), | |
| "risk_sublabel": ps("rsl", | |
| fontSize=7, leading=10, textColor=TEXT_MUTED, alignment=TA_CENTER, | |
| ), | |
| "signal_item": ps("si", fontSize=8, leading=13, textColor=TEXT_PRIMARY, | |
| leftIndent=10), | |
| "disclaimer": ps("disc", fontSize=7, leading=10, textColor=TEXT_DIM, | |
| alignment=TA_CENTER), | |
| "meta_key": ps("mk", fontName="Helvetica-Bold", fontSize=7, | |
| textColor=TEXT_MUTED, leading=11), | |
| "meta_val": ps("mv", fontSize=8, textColor=TEXT_PRIMARY, leading=11), | |
| "table_header": ps("th", fontName="Helvetica-Bold", fontSize=7, | |
| textColor=ACCENT_CYAN, leading=10), | |
| "table_cell": ps("tc", fontSize=8, textColor=TEXT_PRIMARY, leading=11), | |
| } | |
| # ββ Section header helper βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _section(title, styles, elements): | |
| elements.append(Spacer(1, 4)) | |
| elements.append(Paragraph(f"βΈ {title.upper()}", styles["section_label"])) | |
| elements.append(HRFlowable( | |
| width="100%", thickness=0.5, | |
| color=BORDER_COLOR, spaceAfter=8, spaceBefore=2 | |
| )) | |
| # ββ Dark panel table helper βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _panel(inner_elements, bg=None, border=BORDER_COLOR, padding=10): | |
| """Wraps content in a dark-background rounded-ish table cell.""" | |
| bg = bg or PANEL_BG | |
| t = Table([[inner_elements]], colWidths=["100%"]) | |
| t.setStyle(TableStyle([ | |
| ("BACKGROUND", (0, 0), (-1, -1), bg), | |
| ("BOX", (0, 0), (-1, -1), 0.5, border), | |
| ("TOPPADDING", (0, 0), (-1, -1), padding), | |
| ("BOTTOMPADDING",(0,0), (-1, -1), padding), | |
| ("LEFTPADDING", (0, 0), (-1, -1), padding), | |
| ("RIGHTPADDING",(0, 0), (-1, -1), padding), | |
| ])) | |
| return t | |
| # ββ Main generator ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_pdf(data, filepath): | |
| W, H = A4 | |
| timestamp = datetime.now().strftime("%d %B %Y %H:%M UTC+5:30") | |
| report_id = datetime.now().strftime("SIB-%Y%m%d-%H%M%S") | |
| # Margins: leave room for header (58+2=60) and footer (36+1=37) | |
| doc = SimpleDocTemplate( | |
| filepath, | |
| pagesize=A4, | |
| leftMargin=40, | |
| rightMargin=40, | |
| topMargin=75, | |
| bottomMargin=50, | |
| ) | |
| styles = _styles() | |
| elements = [] | |
| # ββ 1. CLASSIFICATION BANNER ββββββββββββββββββββββββββββββββββββββββββββββ | |
| banner = Table( | |
| [[ Paragraph("β¬ THREAT INTELLIGENCE REPORT β CONFIDENTIAL", ParagraphStyle( | |
| "banner", fontName="Helvetica-Bold", fontSize=8, | |
| textColor=ACCENT_CYAN, alignment=TA_CENTER, leading=10 | |
| )) ]], | |
| colWidths=[W - 80] | |
| ) | |
| banner.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), DARK_BG), | |
| ("BOX", (0,0),(-1,-1), 1, ACCENT_CYAN), | |
| ("TOPPADDING", (0,0),(-1,-1), 6), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 6), | |
| ])) | |
| elements.append(banner) | |
| elements.append(Spacer(1, 14)) | |
| # ββ 2. METADATA ROW βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| meta_rows = [ | |
| [ | |
| Paragraph("CLASSIFICATION", styles["meta_key"]), | |
| Paragraph("REPORT TYPE", styles["meta_key"]), | |
| Paragraph("ANALYSIS ENGINE", styles["meta_key"]), | |
| Paragraph("TIMESTAMP", styles["meta_key"]), | |
| ], | |
| [ | |
| Paragraph("RESTRICTED", styles["meta_val"]), | |
| Paragraph("Digital Threat Assessment", styles["meta_val"]), | |
| Paragraph("SentinelAI v2 Β· Transformer NLP", styles["meta_val"]), | |
| Paragraph(timestamp, styles["meta_val"]), | |
| ], | |
| ] | |
| meta_table = Table(meta_rows, colWidths=[(W - 80) / 4] * 4) | |
| meta_table.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), PANEL_BG), | |
| ("BOX", (0,0),(-1,-1), 0.5, BORDER_COLOR), | |
| ("LINEBELOW", (0,0),(-1,0), 0.5, BORDER_COLOR), | |
| ("LINEBEFORE", (1,0),(3,-1), 0.5, BORDER_COLOR), | |
| ("TOPPADDING", (0,0),(-1,-1), 7), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 7), | |
| ("LEFTPADDING", (0,0),(-1,-1), 10), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 10), | |
| ])) | |
| elements.append(meta_table) | |
| elements.append(Spacer(1, 18)) | |
| # ββ 3. RISK ASSESSMENT PANEL ββββββββββββββββββββββββββββββββββββββββββββββ | |
| risk_level = data.get("risk_level", "LOW") | |
| risk_score = float(data.get("risk_score", 0)) | |
| if risk_level == "HIGH": | |
| risk_bg, risk_fg, risk_bar = RISK_HIGH_BG, RISK_HIGH_FG, RISK_HIGH_BAR | |
| risk_label_text = "HIGH RISK β SCAM DETECTED" | |
| risk_desc = "This message exhibits strong indicators of a digital arrest or financial scam. Immediate action is advised." | |
| threat_icon = "π΄" | |
| elif risk_level == "MEDIUM": | |
| risk_bg, risk_fg, risk_bar = RISK_MED_BG, RISK_MED_FG, RISK_MED_BAR | |
| risk_label_text = "MEDIUM RISK β SUSPICIOUS" | |
| risk_desc = "This message contains several suspicious patterns. Exercise caution and verify through official channels." | |
| threat_icon = "π‘" | |
| else: | |
| risk_bg, risk_fg, risk_bar = RISK_LOW_BG, RISK_LOW_FG, RISK_LOW_BAR | |
| risk_label_text = "LOW RISK β LIKELY SAFE" | |
| risk_desc = "No significant scam indicators detected. The message appears to be legitimate communication." | |
| threat_icon = "π’" | |
| score_style = ParagraphStyle("score_dyn", fontName="Helvetica-Bold", | |
| fontSize=42, leading=48, textColor=risk_fg, | |
| alignment=TA_CENTER) | |
| label_style = ParagraphStyle("label_dyn", fontName="Helvetica-Bold", | |
| fontSize=10, leading=14, textColor=risk_fg, | |
| alignment=TA_CENTER) | |
| desc_style = ParagraphStyle("desc_dyn", fontSize=8, leading=12, | |
| textColor=TEXT_MUTED, alignment=TA_CENTER) | |
| # Progress bar simulation via a two-cell table | |
| bar_filled = max(2, int((risk_score / 100) * 100)) | |
| bar_empty = 100 - bar_filled | |
| bar_table = Table( | |
| [["", ""]], | |
| colWidths=[((W - 80 - 40) * bar_filled / 100), | |
| ((W - 80 - 40) * bar_empty / 100)] | |
| ) | |
| bar_table.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(0,0), risk_bar), | |
| ("BACKGROUND", (1,0),(1,0), BORDER_COLOR), | |
| ("TOPPADDING", (0,0),(-1,-1), 3), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 3), | |
| ("LEFTPADDING", (0,0),(-1,-1), 0), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 0), | |
| ])) | |
| risk_inner = [ | |
| Paragraph(f"{threat_icon} {risk_label_text}", label_style), | |
| Spacer(1, 6), | |
| Paragraph(f"{risk_score:.1f}%", score_style), | |
| Spacer(1, 8), | |
| bar_table, | |
| Spacer(1, 8), | |
| Paragraph(risk_desc, desc_style), | |
| ] | |
| risk_panel = Table([[risk_inner]], colWidths=[W - 80]) | |
| risk_panel.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), risk_bg), | |
| ("BOX", (0,0),(-1,-1), 1, risk_bar), | |
| ("TOPPADDING", (0,0),(-1,-1), 16), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 16), | |
| ("LEFTPADDING", (0,0),(-1,-1), 20), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 20), | |
| ])) | |
| elements.append(risk_panel) | |
| elements.append(Spacer(1, 18)) | |
| # ββ 4. ANALYZED MESSAGE βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _section("01 Β· Intercepted Communication", styles, elements) | |
| msg_text = data.get("message", "No message provided.") | |
| msg_para = Paragraph(msg_text, ParagraphStyle( | |
| "msg", fontSize=9, leading=15, textColor=TEXT_PRIMARY, | |
| fontName="Courier", leftIndent=0 | |
| )) | |
| msg_panel = Table([[msg_para]], colWidths=[W - 80]) | |
| msg_panel.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), PANEL_BG), | |
| ("LINEAFTER", (0,0),(0,-1), 3, ACCENT_CYAN), # left accent bar | |
| ("BOX", (0,0),(-1,-1), 0.5, BORDER_COLOR), | |
| ("TOPPADDING", (0,0),(-1,-1), 12), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 12), | |
| ("LEFTPADDING", (0,0),(-1,-1), 14), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 14), | |
| ])) | |
| elements.append(msg_panel) | |
| elements.append(Spacer(1, 14)) | |
| # ββ 5. DETECTED SIGNALS βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _section("02 Β· Detected Risk Indicators", styles, elements) | |
| signals = data.get("signals", []) | |
| if signals: | |
| signal_rows = [] | |
| for i, sig in enumerate(signals, 1): | |
| signal_rows.append([ | |
| Paragraph(f"{i:02d}", ParagraphStyle( | |
| "snum", fontName="Helvetica-Bold", fontSize=8, | |
| textColor=ACCENT_CYAN, alignment=TA_CENTER, leading=12 | |
| )), | |
| Paragraph(f"β {sig}", ParagraphStyle( | |
| "stxt", fontSize=8, leading=13, textColor=TEXT_PRIMARY | |
| )), | |
| ]) | |
| sig_table = Table(signal_rows, colWidths=[30, W - 80 - 30]) | |
| sig_table.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), PANEL_BG), | |
| ("BOX", (0,0),(-1,-1), 0.5, BORDER_COLOR), | |
| ("LINEBELOW", (0,0),(-1,-2), 0.3, BORDER_COLOR), | |
| ("VALIGN", (0,0),(-1,-1), "MIDDLE"), | |
| ("TOPPADDING", (0,0),(-1,-1), 7), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 7), | |
| ("LEFTPADDING", (0,0),(-1,-1), 10), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 10), | |
| ("BACKGROUND", (0,0),(0,-1), colors.HexColor("#0D1F2D")), | |
| ])) | |
| elements.append(sig_table) | |
| else: | |
| no_sig = Table( | |
| [[Paragraph("β No high-confidence scam indicators detected in this message.", ParagraphStyle( | |
| "nosig", fontSize=8, leading=12, textColor=RISK_LOW_FG | |
| ))]], | |
| colWidths=[W - 80] | |
| ) | |
| no_sig.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), RISK_LOW_BG), | |
| ("BOX", (0,0),(-1,-1), 0.5, RISK_LOW_BAR), | |
| ("TOPPADDING", (0,0),(-1,-1), 10), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 10), | |
| ("LEFTPADDING", (0,0),(-1,-1), 14), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 14), | |
| ])) | |
| elements.append(no_sig) | |
| elements.append(Spacer(1, 14)) | |
| # ββ 6. AI INTERPRETATION ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _section("03 Β· AI Model Interpretation", styles, elements) | |
| interp_rows = [ | |
| ["Model Architecture", "Fine-tuned Transformer (BERT-class) Β· Digital Scam Corpus"], | |
| ["Detection Method", "Linguistic pattern matching + structural scam framework analysis"], | |
| ["Confidence Basis", f"{max(risk_score, 100 - risk_score):.1f}% model confidence on primary classification"], | |
| ["Threat Category", "Digital Arrest Scam / Impersonation / Financial Coercion"], | |
| ] | |
| interp_table = Table( | |
| [[Paragraph(k, styles["meta_key"]), Paragraph(v, styles["table_cell"])] | |
| for k, v in interp_rows], | |
| colWidths=[130, W - 80 - 130] | |
| ) | |
| interp_table.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), PANEL_BG), | |
| ("BACKGROUND", (0,0),(0,-1), colors.HexColor("#0D1F2D")), | |
| ("BOX", (0,0),(-1,-1), 0.5, BORDER_COLOR), | |
| ("LINEBELOW", (0,0),(-1,-2), 0.3, BORDER_COLOR), | |
| ("LINEAFTER", (0,0),(0,-1), 0.5, BORDER_COLOR), | |
| ("VALIGN", (0,0),(-1,-1), "MIDDLE"), | |
| ("TOPPADDING", (0,0),(-1,-1), 7), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 7), | |
| ("LEFTPADDING", (0,0),(-1,-1), 10), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 10), | |
| ])) | |
| elements.append(interp_table) | |
| elements.append(Spacer(1, 14)) | |
| # ββ 7. RECOMMENDED ACTIONS ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _section("04 Β· Recommended Immediate Actions", styles, elements) | |
| actions = [ | |
| ("CRITICAL", "Do NOT transfer funds, share OTP, passwords, or any personal credentials."), | |
| ("CRITICAL", "Disconnect immediately from the suspicious communication channel."), | |
| ("HIGH", "Verify the sender's identity through official government or bank websites only."), | |
| ("HIGH", "Report the incident to the National Cybercrime Portal: cybercrime.gov.in"), | |
| ("MEDIUM", "Preserve all evidence β screenshots, call logs, and message history."), | |
| ("MEDIUM", "Alert family members and close contacts about this threat pattern."), | |
| ] | |
| priority_colors = { | |
| "CRITICAL": (colors.HexColor("#3D1515"), RISK_HIGH_FG), | |
| "HIGH": (colors.HexColor("#2D2008"), RISK_MED_FG), | |
| "MEDIUM": (colors.HexColor("#0D2818"), RISK_LOW_FG), | |
| } | |
| action_data = [] | |
| for priority, text in actions: | |
| bg, fg = priority_colors[priority] | |
| action_data.append([ | |
| Paragraph(priority, ParagraphStyle( | |
| "pri", fontName="Helvetica-Bold", fontSize=6, | |
| textColor=fg, alignment=TA_CENTER, leading=9 | |
| )), | |
| Paragraph(text, ParagraphStyle( | |
| "act", fontSize=8, leading=13, textColor=TEXT_PRIMARY | |
| )), | |
| ]) | |
| action_table = Table(action_data, colWidths=[55, W - 80 - 55]) | |
| action_table.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), PANEL_BG), | |
| ("BOX", (0,0),(-1,-1), 0.5, BORDER_COLOR), | |
| ("LINEBELOW", (0,0),(-1,-2), 0.3, BORDER_COLOR), | |
| ("LINEAFTER", (0,0),(0,-1), 0.5, BORDER_COLOR), | |
| ("VALIGN", (0,0),(-1,-1), "MIDDLE"), | |
| ("TOPPADDING", (0,0),(-1,-1), 7), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 7), | |
| ("LEFTPADDING", (0,0),(-1,-1), 8), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 8), | |
| # Per-row background for priority column | |
| ("BACKGROUND", (0,0),(0,1), colors.HexColor("#3D1515")), | |
| ("BACKGROUND", (0,2),(0,3), colors.HexColor("#2D2008")), | |
| ("BACKGROUND", (0,4),(0,5), colors.HexColor("#0D2818")), | |
| ])) | |
| elements.append(action_table) | |
| elements.append(Spacer(1, 20)) | |
| # ββ 8. DISCLAIMER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| disc_table = Table( | |
| [[Paragraph( | |
| "DISCLAIMER Β· SentinelAI provides AI-based probabilistic risk estimation and does not constitute " | |
| "legal advice. All findings are based on pattern recognition and should be verified through official " | |
| "law enforcement or financial authorities. This report is generated for informational purposes only.", | |
| styles["disclaimer"] | |
| )]], | |
| colWidths=[W - 80] | |
| ) | |
| disc_table.setStyle(TableStyle([ | |
| ("BACKGROUND", (0,0),(-1,-1), DARK_BG), | |
| ("BOX", (0,0),(-1,-1), 0.5, BORDER_COLOR), | |
| ("TOPPADDING", (0,0),(-1,-1), 10), | |
| ("BOTTOMPADDING",(0,0),(-1,-1), 10), | |
| ("LEFTPADDING", (0,0),(-1,-1), 14), | |
| ("RIGHTPADDING", (0,0),(-1,-1), 14), | |
| ])) | |
| elements.append(disc_table) | |
| # ββ Build βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| doc.build( | |
| elements, | |
| onFirstPage=lambda c, d: _draw_page(c, d, report_id, timestamp), | |
| onLaterPages=lambda c, d: _draw_page(c, d, report_id, timestamp), | |
| ) |