rtm-class-ai / src /agent /lkpd_pdf.py
nbintang
initialize projects
62516b8
Raw
History Blame Contribute Delete
9.1 kB
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from typing import Any
from src.agent.types import LkpdContent, MaterialInfo
from src.config import settings
_HEADER_HEIGHT = 74
_FIRST_PAGE_EXTRAS_HEIGHT = 106
def _resolve_logo_path() -> str | None:
raw = settings.lkpd_header_logo_path.strip()
if not raw:
return None
candidate = Path(raw)
if not candidate.is_absolute():
candidate = Path.cwd() / candidate
if candidate.exists() and candidate.is_file():
return str(candidate)
return None
def _draw_student_identity_block(canvas: Any, doc: Any, y_top: float) -> None:
from reportlab.lib import colors
box_x = doc.leftMargin
box_w = doc.pagesize[0] - doc.leftMargin - doc.rightMargin
box_h = 44
box_y = y_top - box_h
mid_x = box_x + (box_w / 2)
row_y = box_y + (box_h / 2)
canvas.setStrokeColor(colors.HexColor("#7A869A"))
canvas.setLineWidth(0.8)
canvas.roundRect(box_x, box_y, box_w, box_h, 4, stroke=1, fill=0)
canvas.line(mid_x, box_y, mid_x, box_y + box_h)
canvas.line(box_x, row_y, box_x + box_w, row_y)
canvas.setFillColor(colors.HexColor("#2B2D42"))
canvas.setFont("Helvetica", 9)
canvas.drawString(box_x + 8, row_y + 5, "Nama:")
canvas.drawString(mid_x + 8, row_y + 5, "NIS:")
canvas.drawString(box_x + 8, box_y + 5, "Kelas:")
canvas.drawString(mid_x + 8, box_y + 5, "Tanggal:")
def _draw_header_brand(
canvas: Any,
doc: Any,
*,
logo_path: str | None,
accent_color: Any,
title_line1: str,
title_line2: str,
title_line3: str,
) -> None:
from reportlab.lib import colors
page_w, page_h = doc.pagesize
header_y = page_h - _HEADER_HEIGHT
canvas.saveState()
canvas.setFillColor(accent_color)
canvas.rect(0, header_y, page_w, _HEADER_HEIGHT, stroke=0, fill=1)
canvas.setStrokeColor(colors.HexColor("#D1D5DB"))
canvas.setLineWidth(0.5)
canvas.line(0, header_y, page_w, header_y)
logo_x = doc.leftMargin
logo_y = header_y + 12
logo_size = _HEADER_HEIGHT - 24
if logo_path:
try:
canvas.drawImage(
logo_path,
logo_x,
logo_y,
width=logo_size,
height=logo_size,
preserveAspectRatio=True,
mask="auto",
)
except Exception:
pass
title_lines = [title_line1.strip(), title_line2.strip()]
third_line = title_line3.strip()
if third_line:
title_lines.append(third_line)
y_start = page_h - 24
for idx, line in enumerate(title_lines):
if idx == 0:
canvas.setFont("Helvetica-Bold", 12)
elif idx == 1:
canvas.setFont("Helvetica-Bold", 11)
else:
canvas.setFont("Helvetica-Bold", 10)
canvas.setFillColor(colors.white)
canvas.drawCentredString(page_w / 2, y_start - (idx * 15), line)
canvas.restoreState()
def _draw_first_page_extras(
canvas: Any,
doc: Any,
*,
material: MaterialInfo,
document_id: str,
) -> None:
from reportlab.lib import colors
page_h = doc.pagesize[1]
extras_top = page_h - _HEADER_HEIGHT - 10
canvas.saveState()
canvas.setFillColor(colors.HexColor("#374151"))
canvas.setFont("Helvetica", 9)
canvas.drawString(doc.leftMargin, extras_top - 12, f"Document ID: {document_id}")
canvas.drawString(
doc.leftMargin,
extras_top - 25,
f"Sumber: {material.filename} ({material.file_type})",
)
_draw_student_identity_block(canvas, doc, y_top=extras_top - 34)
canvas.restoreState()
def _draw_first_page(
canvas: Any,
doc: Any,
*,
material: MaterialInfo,
document_id: str,
logo_path: str | None,
accent_color: Any,
title_line1: str,
title_line2: str,
title_line3: str,
) -> None:
_draw_header_brand(
canvas,
doc,
logo_path=logo_path,
accent_color=accent_color,
title_line1=title_line1,
title_line2=title_line2,
title_line3=title_line3,
)
_draw_first_page_extras(canvas, doc, material=material, document_id=document_id)
def _draw_later_page(
canvas: Any,
doc: Any,
*,
logo_path: str | None,
accent_color: Any,
title_line1: str,
title_line2: str,
title_line3: str,
) -> None:
_draw_header_brand(
canvas,
doc,
logo_path=logo_path,
accent_color=accent_color,
title_line1=title_line1,
title_line2=title_line2,
title_line3=title_line3,
)
def render_lkpd_pdf(
*,
lkpd: LkpdContent,
material: MaterialInfo,
document_id: str,
) -> bytes:
try:
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Paragraph, Spacer
except ImportError as exc:
raise RuntimeError("reportlab package is required for LKPD PDF rendering.") from exc
try:
accent_color = colors.HexColor(settings.lkpd_header_accent_hex)
except Exception:
accent_color = colors.HexColor("#1F4E79")
logo_path = _resolve_logo_path()
title_line1 = settings.lkpd_header_title_line1.strip() or "LEMBAR KERJA PESERTA DIDIK (LKPD)"
title_line2 = settings.lkpd_header_title_line2.strip() or "SMARTER AI"
title_line3 = settings.lkpd_header_title_line3.strip()
buffer = BytesIO()
doc = BaseDocTemplate(
buffer,
pagesize=A4,
leftMargin=40,
rightMargin=40,
topMargin=40,
bottomMargin=40,
)
page_w, page_h = A4
frame_width = page_w - doc.leftMargin - doc.rightMargin
first_frame_top = page_h - _HEADER_HEIGHT - _FIRST_PAGE_EXTRAS_HEIGHT - 12
first_frame_height = first_frame_top - doc.bottomMargin
first_frame = Frame(
x1=doc.leftMargin,
y1=doc.bottomMargin,
width=frame_width,
height=first_frame_height,
id="lkpd-first-frame",
showBoundary=0,
)
later_frame_top = page_h - _HEADER_HEIGHT - 12
later_frame_height = later_frame_top - doc.bottomMargin
later_frame = Frame(
x1=doc.leftMargin,
y1=doc.bottomMargin,
width=frame_width,
height=later_frame_height,
id="lkpd-later-frame",
showBoundary=0,
)
first_template = PageTemplate(
id="lkpd-first-page",
frames=[first_frame],
autoNextPageTemplate="lkpd-later-page",
onPage=lambda canvas, page_doc: _draw_first_page(
canvas,
page_doc,
material=material,
document_id=document_id,
logo_path=logo_path,
accent_color=accent_color,
title_line1=title_line1,
title_line2=title_line2,
title_line3=title_line3,
),
)
later_template = PageTemplate(
id="lkpd-later-page",
frames=[later_frame],
onPage=lambda canvas, page_doc: _draw_later_page(
canvas,
page_doc,
logo_path=logo_path,
accent_color=accent_color,
title_line1=title_line1,
title_line2=title_line2,
title_line3=title_line3,
),
)
doc.addPageTemplates([first_template, later_template])
styles = getSampleStyleSheet()
elements = [
Paragraph(lkpd.title, styles["Title"]),
Spacer(1, 8),
Paragraph("Tujuan Pembelajaran", styles["Heading2"]),
]
for idx, item in enumerate(lkpd.learning_objectives, start=1):
elements.append(Paragraph(f"{idx}. {item}", styles["Normal"]))
elements.extend([Spacer(1, 12), Paragraph("Petunjuk", styles["Heading2"])])
for idx, item in enumerate(lkpd.instructions, start=1):
elements.append(Paragraph(f"{idx}. {item}", styles["Normal"]))
elements.extend([Spacer(1, 12), Paragraph("Aktivitas", styles["Heading2"])])
for activity in lkpd.activities:
elements.append(
Paragraph(
f"{activity.activity_no}. {activity.task}",
styles["Heading3"],
)
)
elements.append(Paragraph(f"Output: {activity.expected_output}", styles["Normal"]))
elements.append(Paragraph(f"Hint Penilaian: {activity.assessment_hint}", styles["Normal"]))
elements.append(Spacer(1, 6))
elements.extend([Spacer(1, 12), Paragraph("Template Lembar Jawaban", styles["Heading2"])])
elements.append(Paragraph(lkpd.worksheet_template, styles["Normal"]))
elements.extend([Spacer(1, 12), Paragraph("Rubrik Penilaian", styles["Heading2"])])
for idx, rubric in enumerate(lkpd.assessment_rubric, start=1):
elements.append(
Paragraph(
f"{idx}. {rubric.aspect} - {rubric.criteria} (Skor: {rubric.score_range})",
styles["Normal"],
)
)
doc.build(elements)
return buffer.getvalue()