Hussien Haider commited on
Commit
ddbf80f
·
1 Parent(s): fee6fb4

redesign: cyber/tech dark theme certificate - dark bg, hexagons, gold accents

Browse files
Files changed (1) hide show
  1. app/services/certificate_service.py +200 -167
app/services/certificate_service.py CHANGED
@@ -220,25 +220,25 @@ def build_certificate_pdf(
220
  from reportlab.pdfgen import canvas
221
  import qrcode
222
  from qrcode.image.pil import PilImage
 
223
 
224
  W, H = landscape(A4)
225
  buf = io.BytesIO()
226
  c = canvas.Canvas(buf, pagesize=landscape(A4))
227
 
228
- # ── Modern color palette ──
229
- DEEP_NAVY = HexColor("#0B1426")
230
- MID_NAVY = HexColor("#162032")
231
- GOLD = HexColor("#D4A853")
232
- GOLD_LIGHT = HexColor("#E8C97A")
233
- TEAL = HexColor("#0D9488")
234
- TEXT_DARK = HexColor("#0F172A")
235
- TEXT_MED = HexColor("#475569")
236
- TEXT_LIGHT = HexColor("#94A3B8")
237
- BG_LIGHT = HexColor("#F8FAFC")
238
- BORDER = HexColor("#E2E8F0")
239
- WHITE = HexColor("#FFFFFF")
240
-
241
- # Register Arabic font
242
  ar_font, ar_font_bold = _ensure_arabic_font()
243
  en_font = "Helvetica"
244
  en_font_bold = "Helvetica-Bold"
@@ -259,197 +259,223 @@ def build_certificate_pdf(
259
  def _tx(text):
260
  return _ar(text) if _is_ar(text) else text
261
 
262
- # ── Clean white background ──
263
- c.setFillColor(WHITE)
264
- c.rect(0, 0, W, H, fill=1, stroke=0)
265
-
266
- # subtle top accent line
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  c.setFillColor(GOLD)
268
- c.rect(0, H - 1.2*mm, W, 1.2*mm, fill=1, stroke=0)
269
 
270
- # ── Sidebar (50 mm) — deep navy ──
271
- BAR = 50 * mm
272
- c.setFillColor(DEEP_NAVY)
273
- c.rect(0, 0, BAR, H, fill=1, stroke=0)
274
 
275
- # gold accent strip on sidebar right edge
276
- c.setFillColor(GOLD)
277
- c.rect(BAR - 1.2*mm, 0, 1.2*mm, H, fill=1, stroke=0)
 
278
 
279
- # subtle gradient overlay on sidebar (lighter at top)
280
- for i in range(40):
281
- alpha = 0.03 * (1 - i / 40)
282
- c.setFillColor(Color(1, 1, 1, alpha))
283
- c.rect(0, H - i * 8*mm, BAR, 8*mm, fill=1, stroke=0)
 
284
 
285
- # ── Logo: ALPHA-LOGO.png ──
286
  if _LOGO_JPG_PATH and os.path.exists(_LOGO_JPG_PATH):
287
- logo_size = 20 * mm
288
- logo_y = H - 34 * mm
289
- c.drawImage(_LOGO_JPG_PATH, (BAR - logo_size) / 2, logo_y,
290
  width=logo_size, height=logo_size, preserveAspectRatio=True)
291
 
292
- # ALPHA TEAM text
293
- c.setFillColor(GOLD)
294
- c.setFont(_font("bold"), 9)
295
- c.drawCentredString(BAR/2, H - 44*mm, _tx("ALPHA TEAM"))
296
- c.setFillColor(TEXT_LIGHT)
297
- c.setFont(_font(), 6.5)
298
- c.drawCentredString(BAR/2, H - 48*mm, _tx("CYBERSECURITY"))
299
 
300
- # sidebar divider — thin gold line
301
- c.setStrokeColor(GOLD)
302
- c.setLineWidth(0.3)
303
- div_y = H - 56*mm
304
- c.line(12*mm, div_y, BAR - 12*mm, div_y)
305
-
306
- # sidebar bottom info
307
- c.setFillColor(TEXT_LIGHT)
308
- c.setFont(_font(), 5.5)
309
- c.drawCentredString(BAR/2, 18*mm, _tx("CyberArena Platform"))
310
- c.drawCentredString(BAR/2, 13*mm, _tx("cyberarena.app"))
311
-
312
- # ── Main content area ──
313
- CX = BAR + (W - BAR) / 2
314
- RIGHT = W - 15*mm
315
- TOP = H - 18*mm
316
-
317
- # Header row: shield + CYBERARENA + subtitle
318
- hdr_x = BAR + 16*mm
319
- hdr_y = TOP + 2*mm
320
-
321
- # small shield icon
322
- c.setFillColor(DEEP_NAVY)
323
- sp2 = c.beginPath()
324
- sp2.moveTo(hdr_x, hdr_y + 5.5*mm)
325
- sp2.lineTo(hdr_x + 3.5*mm, hdr_y + 3.5*mm)
326
- sp2.lineTo(hdr_x + 3.5*mm, hdr_y + 0.5*mm)
327
- sp2.lineTo(hdr_x + 1.5*mm, hdr_y - 1*mm)
328
- sp2.lineTo(hdr_x, hdr_y + 0.5*mm)
329
- sp2.lineTo(hdr_x - 1.5*mm, hdr_y - 1*mm)
330
- sp2.lineTo(hdr_x - 3.5*mm, hdr_y + 0.5*mm)
331
- sp2.lineTo(hdr_x - 3.5*mm, hdr_y + 3.5*mm)
332
- sp2.close()
333
- c.drawPath(sp2, fill=1, stroke=0)
334
- c.setStrokeColor(GOLD)
335
- c.setLineWidth(0.5)
336
- c.rect(hdr_x - 1.2*mm, hdr_y + 0.5*mm, 2.4*mm, 2*mm, fill=0, stroke=1)
337
- c.arc(hdr_x - 0.8*mm, hdr_y + 2*mm, hdr_x + 0.8*mm, hdr_y + 3.5*mm, 0, 180)
338
-
339
- c.setFillColor(DEEP_NAVY)
340
- c.setFont(_font("bold"), 13)
341
- c.drawString(hdr_x + 6*mm, TOP, _tx("CYBERARENA"))
342
- c.setFillColor(TEXT_LIGHT)
343
- c.setFont(_font(), 7.5)
344
- c.drawString(hdr_x + 6*mm, TOP - 4.5*mm, _tx("CERTIFICATE OF COMPLETION"))
345
-
346
- # gold separator line under header
347
- c.setStrokeColor(GOLD)
348
- c.setLineWidth(0.6)
349
- c.line(BAR + 12*mm, TOP - 8*mm, RIGHT, TOP - 8*mm)
350
 
351
- # ── Title block ──
352
- cert_label = "شهادة إتمام" if lang == "ar" else "CERTIFICATE"
353
  cert_label2 = "في الأمن السيبراني" if lang == "ar" else "OF COMPLETION"
354
-
355
- c.setFillColor(DEEP_NAVY)
356
- c.setFont(_font("bold"), 32)
357
- c.drawCentredString(CX, TOP - 30*mm, _tx(cert_label))
358
- c.setFillColor(TEXT_MED)
359
- c.setFont(_font(), 15)
360
  c.drawCentredString(CX, TOP - 38*mm, _tx(cert_label2))
361
 
362
- # modern divider — gold lines with diamond
363
- div3_y = TOP - 44*mm
364
- c.setStrokeColor(GOLD)
365
- c.setLineWidth(0.7)
366
- c.line(CX - 32*mm, div3_y, CX - 2.5*mm, div3_y)
367
- c.line(CX + 2.5*mm, div3_y, CX + 32*mm, div3_y)
368
- # diamond
369
  c.setFillColor(GOLD)
370
  d2 = c.beginPath()
371
- d2.moveTo(CX, div3_y + 2*mm)
372
- d2.lineTo(CX + 2*mm, div3_y)
373
- d2.lineTo(CX, div3_y - 2*mm)
374
- d2.lineTo(CX - 2*mm, div3_y)
375
  d2.close()
376
  c.drawPath(d2, fill=1, stroke=0)
377
 
378
  # ── Body ──
379
- # intro line
380
  intro = "نشهد بأن" if lang == "ar" else "This is to certify that"
381
- c.setFillColor(TEXT_MED)
382
- c.setFont(_font(), 10)
383
  c.drawCentredString(CX, TOP - 56*mm, _tx(intro))
384
 
385
- # user name (large, navy, bold)
386
  name = (user_name or "Trainee")[:60]
387
- c.setFillColor(DEEP_NAVY)
388
- c.setFont(_font_for(name, "bold"), 28)
389
- c.drawCentredString(CX, TOP - 70*mm, _tx(name))
390
 
391
- # gold underline under name
392
- nw = c.stringWidth(_tx(name), _font_for(name, "bold"), 28)
393
  c.setStrokeColor(GOLD)
394
- c.setLineWidth(1.5)
395
- c.line(CX - nw/2 - 2, TOP - 73*mm, CX + nw/2 + 2, TOP - 73*mm)
 
 
 
 
 
396
 
397
  # body text
398
  body = "قد أتمّ بنجاح جميع التحديات التفاعلية والمختبرات العملية في مجال:" if lang == "ar" else "has successfully completed all interactive challenges and practical labs in"
399
- c.setFillColor(TEXT_MED)
400
- c.setFont(_font(), 10)
401
- c.drawCentredString(CX, TOP - 86*mm, _tx(body))
402
 
403
- # category (gold, bold)
404
  cat_label = category_label(category, lang=lang)
 
 
 
 
 
 
 
405
  c.setFillColor(GOLD)
406
- c.setFont(_font("bold"), 20)
407
- c.drawCentredString(CX, TOP - 100*mm, _tx(cat_label))
408
 
409
  # subtitle
410
- c.setFillColor(TEXT_LIGHT)
411
- c.setFont(_font_for(title), 8.5)
412
- c.drawCentredString(CX, TOP - 107*mm, _tx(title))
413
 
414
  # ── Bottom section ──
415
- BOT = 14*mm
416
-
417
- # thin separator above bottom
418
  c.setStrokeColor(BORDER)
419
  c.setLineWidth(0.3)
420
- c.line(BAR + 12*mm, BOT + 38*mm, RIGHT, BOT + 38*mm)
421
 
422
  # 1) QR code
423
  qr_payload = (CERT_VERIFY_BASE_URL or "https://cyberarena.app/verify") + "/verify?code=" + verify_code
424
- qr = qrcode.QRCode(box_size=4, border=0)
425
  qr.add_data(qr_payload)
426
  qr.make(fit=True)
427
- qr_img = qr.make_image(fill_color="#0B1426", back_color="white", image_factory=PilImage)
428
  qr_path = os.path.join(tempfile.gettempdir(), f"ca_qr_{verify_code.replace(' ', '_')}.png")
429
  qr_img.save(qr_path, format="PNG")
430
- qr_s = 20*mm
431
- qr_x = BAR + 14*mm
432
- c.drawImage(qr_path, qr_x, BOT + 2*mm, width=qr_s, height=qr_s, preserveAspectRatio=True, mask="auto")
433
- c.setFillColor(DEEP_NAVY)
 
 
 
 
434
  qr_caption = "SCAN TO VERIFY" if lang == "en" else "امسح للتحقق"
435
- c.setFont(_font_for(qr_caption, "bold"), 6.5)
436
- c.drawCentredString(qr_x + qr_s/2, BOT - 1.5*mm, _tx(qr_caption))
437
- c.setFont(_font_for(verify_code), 5.5)
438
- c.setFillColor(TEXT_LIGHT)
439
- c.drawCentredString(qr_x + qr_s/2, BOT - 5*mm, _tx(verify_code))
440
 
441
  # 2) Info rows
442
- infox = BAR + (W - BAR) * 0.36
443
  date_str = (_format_date_en(issue_date) if lang == "en" else _format_date_ar(issue_date)) or "—"
444
  info_items = [
445
- ("ISSUE DATE" if lang == "en" else "تاريخ الإصدار", date_str, TEXT_DARK, 9),
446
- ("CERTIFICATE ID" if lang == "en" else "رقم الشهادة", str(cert_id)[:28], TEXT_MED, 7),
447
  ("VERIFICATION CODE" if lang == "en" else "رمز التحقق", verify_code, GOLD, 7),
448
  ]
449
  for i, (label, val, vcolor, vsize) in enumerate(info_items):
450
- ypos = BOT + 28*mm - i * 9.5*mm
451
- c.setFillColor(TEXT_LIGHT)
452
- c.setFont(_font_for(label, "bold"), 6.5)
453
  c.drawString(infox, ypos, _tx(label))
454
  c.setFillColor(vcolor)
455
  vfont = "Courier" if not _is_ar(val) and lang == "en" else _font_for(val)
@@ -457,24 +483,31 @@ def build_certificate_pdf(
457
  c.drawString(infox, ypos - 3.5*mm, _tx(val))
458
 
459
  # 3) Signature
460
- sig_x = W - 30*mm
461
  c.setStrokeColor(BORDER)
462
- c.setLineWidth(0.4)
463
- c.line(sig_x - 15*mm, BOT + 20*mm, sig_x + 15*mm, BOT + 20*mm)
464
  sig_label1 = "Alpha Team Academic Board" if lang == "en" else "المجلس الأكاديمي"
465
  sig_label2 = "Alpha Team" if lang == "en" else "فريق ألفا"
466
- c.setFillColor(TEXT_DARK)
467
- c.setFont(_font_for(sig_label1, "bold"), 7)
468
- c.drawCentredString(sig_x, BOT + 15*mm, _tx(sig_label1))
469
- c.setFillColor(TEXT_LIGHT)
470
- c.setFont(_font_for(sig_label2), 6)
471
- c.drawCentredString(sig_x, BOT + 9*mm, _tx(sig_label2))
472
-
473
- # ── Bottom accent bar ──
 
 
 
 
 
 
 
474
  c.setFillColor(GOLD)
475
- c.rect(0, 0, W, 1*mm, fill=1, stroke=0)
476
- c.setFillColor(DEEP_NAVY)
477
- c.rect(0, 1*mm, W, 0.4*mm, fill=1, stroke=0)
478
 
479
  c.showPage()
480
  c.save()
 
220
  from reportlab.pdfgen import canvas
221
  import qrcode
222
  from qrcode.image.pil import PilImage
223
+ import math
224
 
225
  W, H = landscape(A4)
226
  buf = io.BytesIO()
227
  c = canvas.Canvas(buf, pagesize=landscape(A4))
228
 
229
+ # ── Cyber color palette ──
230
+ BG_DARK = HexColor("#080E1A")
231
+ BG_MID = HexColor("#0D1526")
232
+ BG_CARD = HexColor("#111C33")
233
+ GOLD = HexColor("#D4A853")
234
+ GOLD_DIM = HexColor("#A8873A")
235
+ CYBER = HexColor("#22D3EE") # cyan accent
236
+ CYBER_DIM = HexColor("#0E7490")
237
+ TEXT_PRIMARY = HexColor("#F1F5F9")
238
+ TEXT_SECOND = HexColor("#94A3B8")
239
+ TEXT_MUTED = HexColor("#475569")
240
+ BORDER = HexColor("#1E293B")
241
+
 
242
  ar_font, ar_font_bold = _ensure_arabic_font()
243
  en_font = "Helvetica"
244
  en_font_bold = "Helvetica-Bold"
 
259
  def _tx(text):
260
  return _ar(text) if _is_ar(text) else text
261
 
262
+ # ── Background: dark gradient ──
263
+ for y in range(0, int(H), 3):
264
+ t = 1 - y / H
265
+ r = 0.03 + 0.05 * t
266
+ g = 0.05 + 0.07 * t
267
+ b = 0.10 + 0.12 * t
268
+ c.setFillColorRGB(min(r, 1), min(g, 1), min(b, 1))
269
+ c.rect(0, y, W, 3, fill=1, stroke=0)
270
+
271
+ # ── Decorative tech grid ──
272
+ c.setStrokeColor(HexColor("#1A2744"))
273
+ c.setLineWidth(0.15)
274
+ for x in range(0, int(W), 20):
275
+ c.line(x, 0, x, H)
276
+ for y in range(0, int(H), 20):
277
+ c.line(0, y, W, y)
278
+
279
+ # ── Decorative hexagons ──
280
+ def _hexagon(cx, cy, r, stroke_color=None, fill_color=None):
281
+ p = c.beginPath()
282
+ for i in range(6):
283
+ a = math.radians(60 * i - 90)
284
+ px = cx + r * math.cos(a)
285
+ py = cy + r * math.sin(a)
286
+ if i == 0:
287
+ p.moveTo(px, py)
288
+ else:
289
+ p.lineTo(px, py)
290
+ p.close()
291
+ if fill_color:
292
+ c.setFillColor(fill_color)
293
+ c.drawPath(p, fill=1, stroke=0)
294
+ if stroke_color:
295
+ c.setStrokeColor(stroke_color)
296
+ c.setLineWidth(0.3)
297
+ c.drawPath(p, fill=0, stroke=1)
298
+
299
+ _hexagon(W - 25*mm, H - 25*mm, 15*mm, stroke_color=HexColor("#1A2744"))
300
+ _hexagon(W - 35*mm, H - 15*mm, 8*mm, stroke_color=HexColor("#1A2744"))
301
+ _hexagon(35*mm, H - 20*mm, 10*mm, stroke_color=HexColor("#1A2744"))
302
+ _hexagon(W - 40*mm, 30*mm, 12*mm, stroke_color=HexColor("#1A2744"))
303
+
304
+ # ── Top gold accent bar ──
305
  c.setFillColor(GOLD)
306
+ c.rect(0, H - 1.5*mm, W, 1.5*mm, fill=1, stroke=0)
307
 
308
+ # ── Main card (slightly lighter bg) ──
309
+ MARGIN = 10*mm
310
+ c.setFillColor(BG_CARD)
311
+ c.roundRect(MARGIN, MARGIN, W - 2*MARGIN, H - 2*MARGIN - 1.5*mm, 4*mm, fill=1, stroke=0)
312
 
313
+ # thin gold border on card
314
+ c.setStrokeColor(GOLD_DIM)
315
+ c.setLineWidth(0.3)
316
+ c.roundRect(MARGIN, MARGIN, W - 2*MARGIN, H - 2*MARGIN - 1.5*mm, 4*mm, fill=0, stroke=1)
317
 
318
+ # ── Inner content area ──
319
+ CX = W / 2
320
+ INNER_L = MARGIN + 12*mm
321
+ INNER_R = W - MARGIN - 12*mm
322
+ TOP = H - MARGIN - 16*mm
323
+ BOT = MARGIN + 14*mm
324
 
325
+ # ── Top left: ALPHA logo + brand ──
326
  if _LOGO_JPG_PATH and os.path.exists(_LOGO_JPG_PATH):
327
+ logo_size = 16 * mm
328
+ c.drawImage(_LOGO_JPG_PATH, INNER_L, TOP - 2*mm,
 
329
  width=logo_size, height=logo_size, preserveAspectRatio=True)
330
 
331
+ c.setFillColor(TEXT_PRIMARY)
332
+ c.setFont(_font("bold"), 11)
333
+ c.drawString(INNER_L + 20*mm, TOP + 1*mm, _tx("ALPHA TEAM"))
334
+ c.setFillColor(TEXT_MUTED)
335
+ c.setFont(_font(), 7)
336
+ c.drawString(INNER_L + 20*mm, TOP - 5*mm, _tx("CYBERSECURITY"))
 
337
 
338
+ # ── Top right: CYBERARENA brand ──
339
+ c.setFillColor(GOLD)
340
+ c.setFont(_font("bold"), 11)
341
+ c.drawRightString(INNER_R, TOP + 1*mm, _tx("CYBERARENA"))
342
+ c.setFillColor(TEXT_MUTED)
343
+ c.setFont(_font(), 7)
344
+ c.drawRightString(INNER_R, TOP - 5*mm, _tx("CERTIFICATE OF COMPLETION"))
345
+
346
+ # ── Gold divider ──
347
+ div_y = TOP - 12*mm
348
+ c.setStrokeColor(GOLD_DIM)
349
+ c.setLineWidth(0.4)
350
+ c.line(INNER_L, div_y, INNER_R, div_y)
351
+
352
+ # ── Cyber seal (large hexagon) ──
353
+ seal_cx = INNER_R - 5*mm
354
+ seal_cy = (TOP + BOT) / 2 + 15*mm
355
+ _hexagon(seal_cx, seal_cy, 18*mm, stroke_color=HexColor("#D4A853"), fill_color=HexColor("#0D1526"))
356
+ _hexagon(seal_cx, seal_cy, 14*mm, stroke_color=HexColor("#A8873A"))
357
+ _hexagon(seal_cx, seal_cy, 10*mm, stroke_color=HexColor("#D4A853"))
358
+ c.setFillColor(GOLD_DIM)
359
+ c.setFont(_font("bold"), 5)
360
+ c.drawCentredString(seal_cx, seal_cy - 2*mm, "✓")
361
+ c.setFont(_font(), 4.5)
362
+ c.drawCentredString(seal_cx, seal_cy - 6*mm, "VERIFIED")
363
+
364
+ # ── Title ──
365
+ cert_label = "شهادة إتمام" if lang == "ar" else "CERTIFICATE"
366
+ c.setFillColor(GOLD)
367
+ c.setFont(_font("bold"), 34)
368
+ c.drawCentredString(CX, TOP - 30*mm, _tx(cert_label))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
+ # subtitle
 
371
  cert_label2 = "في الأمن السيبراني" if lang == "ar" else "OF COMPLETION"
372
+ c.setFillColor(TEXT_SECOND)
373
+ c.setFont(_font(), 13)
 
 
 
 
374
  c.drawCentredString(CX, TOP - 38*mm, _tx(cert_label2))
375
 
376
+ # ── Gold diamond divider ──
377
+ div2_y = TOP - 44*mm
378
+ c.setStrokeColor(GOLD_DIM)
379
+ c.setLineWidth(0.5)
380
+ c.line(CX - 25*mm, div2_y, CX - 2.5*mm, div2_y)
381
+ c.line(CX + 2.5*mm, div2_y, CX + 25*mm, div2_y)
 
382
  c.setFillColor(GOLD)
383
  d2 = c.beginPath()
384
+ d2.moveTo(CX, div2_y + 1.8*mm)
385
+ d2.lineTo(CX + 1.8*mm, div2_y)
386
+ d2.lineTo(CX, div2_y - 1.8*mm)
387
+ d2.lineTo(CX - 1.8*mm, div2_y)
388
  d2.close()
389
  c.drawPath(d2, fill=1, stroke=0)
390
 
391
  # ── Body ──
 
392
  intro = "نشهد بأن" if lang == "ar" else "This is to certify that"
393
+ c.setFillColor(TEXT_SECOND)
394
+ c.setFont(_font(), 9)
395
  c.drawCentredString(CX, TOP - 56*mm, _tx(intro))
396
 
397
+ # Name (large, gold)
398
  name = (user_name or "Trainee")[:60]
399
+ c.setFillColor(TEXT_PRIMARY)
400
+ c.setFont(_font_for(name, "bold"), 26)
401
+ c.drawCentredString(CX, TOP - 68*mm, _tx(name))
402
 
403
+ # gold underline
404
+ nw = c.stringWidth(_tx(name), _font_for(name, "bold"), 26)
405
  c.setStrokeColor(GOLD)
406
+ c.setLineWidth(1.2)
407
+ c.line(CX - nw/2 - 2, TOP - 71*mm, CX + nw/2 + 2, TOP - 71*mm)
408
+
409
+ # small cyber dots on underline
410
+ c.setFillColor(CYBER)
411
+ for dot_x in [CX - nw/2 - 3, CX + nw/2 + 3]:
412
+ c.circle(dot_x, TOP - 71*mm, 0.6*mm, fill=1, stroke=0)
413
 
414
  # body text
415
  body = "قد أتمّ بنجاح جميع التحديات التفاعلية والمختبرات العملية في مجال:" if lang == "ar" else "has successfully completed all interactive challenges and practical labs in"
416
+ c.setFillColor(TEXT_SECOND)
417
+ c.setFont(_font(), 9)
418
+ c.drawCentredString(CX, TOP - 84*mm, _tx(body))
419
 
420
+ # category badge
421
  cat_label = category_label(category, lang=lang)
422
+ # badge background
423
+ badge_w = c.stringWidth(_tx(cat_label), _font("bold"), 18) + 10*mm
424
+ c.setFillColor(BG_DARK)
425
+ c.roundRect(CX - badge_w/2, TOP - 99*mm - 3*mm, badge_w, 12*mm, 3*mm, fill=1, stroke=0)
426
+ c.setStrokeColor(GOLD_DIM)
427
+ c.setLineWidth(0.5)
428
+ c.roundRect(CX - badge_w/2, TOP - 99*mm - 3*mm, badge_w, 12*mm, 3*mm, fill=0, stroke=1)
429
  c.setFillColor(GOLD)
430
+ c.setFont(_font("bold"), 18)
431
+ c.drawCentredString(CX, TOP - 99*mm, _tx(cat_label))
432
 
433
  # subtitle
434
+ c.setFillColor(TEXT_MUTED)
435
+ c.setFont(_font_for(title), 7.5)
436
+ c.drawCentredString(CX, TOP - 110*mm, _tx(title))
437
 
438
  # ── Bottom section ──
439
+ # divider
 
 
440
  c.setStrokeColor(BORDER)
441
  c.setLineWidth(0.3)
442
+ c.line(INNER_L, BOT + 34*mm, INNER_R, BOT + 34*mm)
443
 
444
  # 1) QR code
445
  qr_payload = (CERT_VERIFY_BASE_URL or "https://cyberarena.app/verify") + "/verify?code=" + verify_code
446
+ qr = qrcode.QRCode(box_size=3, border=0)
447
  qr.add_data(qr_payload)
448
  qr.make(fit=True)
449
+ qr_img = qr.make_image(fill_color="#D4A853", back_color="#111C33", image_factory=PilImage)
450
  qr_path = os.path.join(tempfile.gettempdir(), f"ca_qr_{verify_code.replace(' ', '_')}.png")
451
  qr_img.save(qr_path, format="PNG")
452
+ qr_s = 18*mm
453
+ qr_x = INNER_L
454
+ c.drawImage(qr_path, qr_x, BOT + 4*mm, width=qr_s, height=qr_s, preserveAspectRatio=True, mask="auto")
455
+ # qr border
456
+ c.setStrokeColor(BORDER)
457
+ c.setLineWidth(0.3)
458
+ c.roundRect(qr_x - 1*mm, BOT + 3*mm, qr_s + 2*mm, qr_s + 2*mm, 1.5*mm, fill=0, stroke=1)
459
+ c.setFillColor(TEXT_SECOND)
460
  qr_caption = "SCAN TO VERIFY" if lang == "en" else "امسح للتحقق"
461
+ c.setFont(_font_for(qr_caption, "bold"), 6)
462
+ c.drawCentredString(qr_x + qr_s/2, BOT - 0.5*mm, _tx(qr_caption))
463
+ c.setFillColor(TEXT_MUTED)
464
+ c.setFont(_font_for(verify_code), 5)
465
+ c.drawCentredString(qr_x + qr_s/2, BOT - 4*mm, _tx(verify_code))
466
 
467
  # 2) Info rows
468
+ infox = INNER_L + 30*mm
469
  date_str = (_format_date_en(issue_date) if lang == "en" else _format_date_ar(issue_date)) or "—"
470
  info_items = [
471
+ ("ISSUE DATE" if lang == "en" else "تاريخ الإصدار", date_str, TEXT_PRIMARY, 8),
472
+ ("CERTIFICATE ID" if lang == "en" else "رقم الشهادة", str(cert_id)[:28], TEXT_SECOND, 7),
473
  ("VERIFICATION CODE" if lang == "en" else "رمز التحقق", verify_code, GOLD, 7),
474
  ]
475
  for i, (label, val, vcolor, vsize) in enumerate(info_items):
476
+ ypos = BOT + 27*mm - i * 8.5*mm
477
+ c.setFillColor(TEXT_MUTED)
478
+ c.setFont(_font_for(label, "bold"), 6)
479
  c.drawString(infox, ypos, _tx(label))
480
  c.setFillColor(vcolor)
481
  vfont = "Courier" if not _is_ar(val) and lang == "en" else _font_for(val)
 
483
  c.drawString(infox, ypos - 3.5*mm, _tx(val))
484
 
485
  # 3) Signature
486
+ sig_x = INNER_R
487
  c.setStrokeColor(BORDER)
488
+ c.setLineWidth(0.3)
489
+ c.line(sig_x - 18*mm, BOT + 20*mm, sig_x, BOT + 20*mm)
490
  sig_label1 = "Alpha Team Academic Board" if lang == "en" else "المجلس الأكاديمي"
491
  sig_label2 = "Alpha Team" if lang == "en" else "فريق ألفا"
492
+ c.setFillColor(TEXT_SECOND)
493
+ c.setFont(_font_for(sig_label1, "bold"), 6.5)
494
+ c.drawRightString(sig_x, BOT + 15*mm, _tx(sig_label1))
495
+ c.setFillColor(TEXT_MUTED)
496
+ c.setFont(_font_for(sig_label2), 5.5)
497
+ c.drawRightString(sig_x, BOT + 9*mm, _tx(sig_label2))
498
+
499
+ # small gold star near seal
500
+ c.setFillColor(GOLD)
501
+ for i in range(3):
502
+ sx = seal_cx + 8*mm * math.cos(math.radians(120 * i - 30))
503
+ sy = seal_cy + 8*mm * math.sin(math.radians(120 * i - 30))
504
+ c.circle(sx, sy, 0.4*mm, fill=1, stroke=0)
505
+
506
+ # ── Bottom accent bars ──
507
  c.setFillColor(GOLD)
508
+ c.rect(0, 0, W, 0.8*mm, fill=1, stroke=0)
509
+ c.setFillColor(BG_DARK)
510
+ c.rect(0, 0.8*mm, W, 0.4*mm, fill=1, stroke=0)
511
 
512
  c.showPage()
513
  c.save()