Files
ShooterHub/apps/tools/analyzer/pdf_report.py
2026-04-02 11:24:30 +02:00

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)