96 lines
3.0 KiB
Python
96 lines
3.0 KiB
Python
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)
|