import base64 import io from datetime import datetime from fpdf import FPDF _COL_LABEL = 80 _COL_VALUE = 50 _ROW_H = 7 def generate_pdf(overall: dict, group_stats: list, charts: list, overview_chart: str) -> bytes: pdf = FPDF() pdf.set_auto_page_break(auto=True, margin=15) pdf.add_page() _title_block(pdf) _overall_section(pdf, overall, overview_chart) for stat, chart_b64 in zip(group_stats, charts): _group_section(pdf, stat, chart_b64) return bytes(pdf.output()) # --------------------------------------------------------------------------- def _title_block(pdf: FPDF): pdf.set_font("Helvetica", "B", 18) pdf.cell(0, 12, "Ballistic Analysis Report", new_x="LMARGIN", new_y="NEXT", align="C") pdf.set_font("Helvetica", "", 9) pdf.cell( 0, 5, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", new_x="LMARGIN", new_y="NEXT", align="C", ) pdf.ln(8) def _overall_section(pdf: FPDF, overall: dict, overview_chart: str): _section_heading(pdf, "Overall Statistics") rows = [ ("Total shots", str(overall["count"])), ("Min speed", f"{overall['min_speed']:.4f}"), ("Max speed", f"{overall['max_speed']:.4f}"), ("Mean speed", f"{overall['mean_speed']:.4f}"), ("Std dev (speed)", f"{overall['std_speed']:.4f}" if overall["std_speed"] is not None else "n/a"), ] _table(pdf, rows) img_bytes = base64.b64decode(overview_chart) pdf.image(io.BytesIO(img_bytes), x=pdf.l_margin, w=min(140, pdf.epw)) pdf.ln(4) def _group_section(pdf: FPDF, stat: dict, chart_b64: str): pdf.ln(4) heading = ( f"Group {stat['group_index']} - " f"{stat['time_start']} to {stat['time_end']} " f"({stat['count']} shot(s))" ) _section_heading(pdf, heading) rows = [ ("Min speed", f"{stat['min_speed']:.4f}"), ("Max speed", f"{stat['max_speed']:.4f}"), ("Mean speed", f"{stat['mean_speed']:.4f}"), ("Std dev (speed)", f"{stat['std_speed']:.4f}" if stat["std_speed"] is not None else "n/a"), ] _table(pdf, rows) img_bytes = base64.b64decode(chart_b64) # Check remaining page space; add new page if chart won't fit if pdf.get_y() + 75 > pdf.page_break_trigger: pdf.add_page() pdf.image(io.BytesIO(img_bytes), x=pdf.l_margin, w=pdf.epw) pdf.ln(4) def _section_heading(pdf: FPDF, text: str): pdf.set_font("Helvetica", "B", 12) pdf.set_fill_color(230, 236, 255) pdf.cell(0, 8, text, new_x="LMARGIN", new_y="NEXT", fill=True) pdf.ln(2) def _table(pdf: FPDF, rows: list): for i, (label, value) in enumerate(rows): fill = i % 2 == 0 pdf.set_fill_color(248, 249, 252) if fill else pdf.set_fill_color(255, 255, 255) pdf.set_font("Helvetica", "", 10) pdf.cell(_COL_LABEL, _ROW_H, label, border=0, fill=fill) pdf.set_font("Helvetica", "B", 10) pdf.cell(_COL_VALUE, _ROW_H, value, border=0, fill=fill, new_x="LMARGIN", new_y="NEXT") pdf.ln(3)