AXe Skills HubSearch /

← All skills

reportlab-pdf-factory

AXe First-party 

Reference: full SKILL.md

Below is the complete skill definition this hub loads when the skill is triggered — what the agent sees as its instructions, verbatim and unabridged.

ReportLab PDF Factory — Professional Report Generation

IMI delivers insight through documents. This skill gives Claude the complete toolkit

for generating professional, branded PDF reports programmatically — from a single-page

topline to a 40-page research report with embedded charts and data tables.

Two PDF Generation Approaches

Approach 1: ReportLab (Canvas + Platypus)

Best for: pixel-precise layouts, complex multi-column designs, direct chart embedding.

Approach 2: WeasyPrint (HTML → PDF)

Best for: complex text-heavy documents, when you already have HTML/CSS output,

faster to write but less control over pagination.

ReportLab — Full IMI Report Template

from reportlab.lib.pagesizes import A4, letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm, inch, cm
from reportlab.lib.colors import HexColor, black, white, grey
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
    PageBreak, KeepTogether, HRFlowable, Image, BalancedColumns
)
from reportlab.graphics.shapes import Drawing, Rect, String
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.charts.linecharts import HorizontalLineChart
from io import BytesIO
from pathlib import Path

# ──────────────────────────────────────────────
# IMI Brand Colors
# ──────────────────────────────────────────────
IMI_NAVY     = HexColor('#1B2B4B')
IMI_BLUE     = HexColor('#0066CC')
IMI_TEAL     = HexColor('#00A89D')
IMI_ORANGE   = HexColor('#F5821E')
IMI_LIGHT_BG = HexColor('#F5F7FA')
IMI_MID_GREY = HexColor('#888888')
IMI_BORDER   = HexColor('#DDDDDD')

# ──────────────────────────────────────────────
# Typography Styles
# ──────────────────────────────────────────────
def get_imi_styles():
    styles = getSampleStyleSheet()

    styles.add(ParagraphStyle(
        'ImiTitle',
        fontSize=28, textColor=IMI_NAVY, spaceAfter=6,
        fontName='Helvetica-Bold', leading=34
    ))
    styles.add(ParagraphStyle(
        'ImiSubtitle',
        fontSize=14, textColor=IMI_TEAL, spaceAfter=12,
        fontName='Helvetica', leading=18
    ))
    styles.add(ParagraphStyle(
        'ImiH1',
        fontSize=16, textColor=IMI_NAVY, spaceBefore=16, spaceAfter=8,
        fontName='Helvetica-Bold', leading=20, borderPad=4
    ))
    styles.add(ParagraphStyle(
        'ImiH2',
        fontSize=13, textColor=IMI_BLUE, spaceBefore=12, spaceAfter=6,
        fontName='Helvetica-Bold', leading=16
    ))
    styles.add(ParagraphStyle(
        'ImiBody',
        fontSize=10, textColor=black, spaceBefore=4, spaceAfter=6,
        fontName='Helvetica', leading=14, alignment=TA_JUSTIFY
    ))
    styles.add(ParagraphStyle(
        'ImiInsight',
        fontSize=11, textColor=IMI_NAVY, spaceBefore=8, spaceAfter=8,
        fontName='Helvetica-Bold', leading=15,
        backColor=IMI_LIGHT_BG, borderColor=IMI_TEAL, borderWidth=1,
        borderPad=8, leftIndent=8
    ))
    styles.add(ParagraphStyle(
        'ImiBullet',
        fontSize=10, textColor=black, spaceBefore=2, spaceAfter=2,
        fontName='Helvetica', leading=14, leftIndent=16, bulletIndent=8
    ))
    styles.add(ParagraphStyle(
        'ImiCaption',
        fontSize=8, textColor=IMI_MID_GREY, spaceAfter=8,
        fontName='Helvetica-Oblique', leading=10, alignment=TA_CENTER
    ))
    return styles


# ──────────────────────────────────────────────
# Header / Footer (Canvas callback)
# ──────────────────────────────────────────────
def make_header_footer(study_name: str, client_name: str, wave: str):
    def _draw(canvas, doc):
        canvas.saveState()
        w, h = A4

        # Header bar
        canvas.setFillColor(IMI_NAVY)
        canvas.rect(0, h - 18*mm, w, 18*mm, fill=1, stroke=0)
        canvas.setFillColor(white)
        canvas.setFont('Helvetica-Bold', 10)
        canvas.drawString(15*mm, h - 11*mm, "IMI International — CONFIDENTIAL")
        canvas.setFont('Helvetica', 9)
        canvas.drawRightString(w - 15*mm, h - 11*mm, f"{study_name} | {wave}")

        # Footer line
        canvas.setStrokeColor(IMI_TEAL)
        canvas.setLineWidth(1)
        canvas.line(15*mm, 14*mm, w - 15*mm, 14*mm)
        canvas.setFillColor(IMI_MID_GREY)
        canvas.setFont('Helvetica', 8)
        canvas.drawString(15*mm, 9*mm, f"© IMI International 2026 | Prepared for {client_name}")
        canvas.drawRightString(w - 15*mm, 9*mm, f"Page {doc.page}")

        canvas.restoreState()
    return _draw


# ──────────────────────────────────────────────
# Tables — metric scorecards
# ──────────────────────────────────────────────
def build_scorecard_table(data: list[dict]) -> Table:
    """
    data = [
        {'metric': 'Overall Appeal', 'score': 67, 'norm': 58, 'vs_norm': '+9', 'flag': '▲'},
        ...
    ]
    """
    headers = ['Metric', 'Score', 'Category Norm', 'vs. Norm', 'Status']
    rows = [headers] + [
        [d['metric'], f"{d['score']}%", f"{d['norm']}%", d['vs_norm'], d['flag']]
        for d in data
    ]

    col_widths = [60*mm, 22*mm, 32*mm, 25*mm, 22*mm]
    table = Table(rows, colWidths=col_widths, repeatRows=1)

    table.setStyle(TableStyle([
        # Header
        ('BACKGROUND', (0, 0), (-1, 0), IMI_NAVY),
        ('TEXTCOLOR',  (0, 0), (-1, 0), white),
        ('FONTNAME',   (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE',   (0, 0), (-1, 0), 9),
        ('ALIGN',      (0, 0), (-1, 0), 'CENTER'),

        # Data rows
        ('FONTNAME',   (0, 1), (-1, -1), 'Helvetica'),
        ('FONTSIZE',   (0, 1), (-1, -1), 9),
        ('ALIGN',      (1, 1), (-1, -1), 'CENTER'),
        ('ALIGN',      (0, 1), (0, -1), 'LEFT'),

        # Alternating row shading
        *[('BACKGROUND', (0, i), (-1, i), IMI_LIGHT_BG) for i in range(2, len(rows), 2)],

        # Grid
        ('GRID',       (0, 0), (-1, -1), 0.5, IMI_BORDER),
        ('TOPPADDING', (0, 0), (-1, -1), 5),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 5),
        ('LEFTPADDING', (0, 0), (-1, -1), 6),
    ]))

    return table


# ──────────────────────────────────────────────
# Charts — bar charts for concept scores
# ──────────────────────────────────────────────
def build_bar_chart(labels: list[str], values: list[float], norm: float,
                    title: str, width: float = 130*mm, height: float = 70*mm) -> Drawing:
    drawing = Drawing(width, height)

    chart = VerticalBarChart()
    chart.x = 20
    chart.y = 20
    chart.width = width - 40
    chart.height = height - 50
    chart.data = [values]
    chart.categoryAxis.categoryNames = labels
    chart.categoryAxis.labels.fontSize = 8
    chart.valueAxis.labels.fontSize = 8
    chart.valueAxis.valueMin = 0
    chart.valueAxis.valueMax = max(max(values), norm) * 1.15
    chart.bars[0].fillColor = IMI_TEAL

    # Norm reference line
    from reportlab.graphics.shapes import Line
    norm_y = chart.y + (norm / chart.valueAxis.valueMax) * chart.height
    drawing.add(Line(chart.x, norm_y, chart.x + chart.width, norm_y,
                     strokeColor=IMI_ORANGE, strokeWidth=1.5, strokeDashArray=[4, 2]))

    # Title
    drawing.add(String(width / 2, height - 15, title, textAnchor='middle',
                        fontSize=9, fontName='Helvetica-Bold', fillColor=IMI_NAVY))

    drawing.add(chart)
    return drawing


# ──────────────────────────────────────────────
# Full Report Generator
# ──────────────────────────────────────────────
def generate_topline_pdf(
    output_path: str,
    study_name: str,
    client_name: str,
    wave: str,
    essential_insight: str,
    findings: list[dict],
    recommendation: str,
    scorecard_data: list[dict] | None = None,
) -> str:
    styles = get_imi_styles()
    doc = SimpleDocTemplate(
        output_path, pagesize=A4,
        leftMargin=15*mm, rightMargin=15*mm,
        topMargin=25*mm, bottomMargin=20*mm
    )

    story = []

    # Cover block
    story.append(Paragraph(f"{study_name}", styles['ImiTitle']))
    story.append(Paragraph(f"Topline Report | {wave} | Prepared for {client_name}", styles['ImiSubtitle']))
    story.append(HRFlowable(width="100%", thickness=2, color=IMI_TEAL, spaceAfter=12))

    # Essential insight callout
    story.append(Paragraph("Essential Insight", styles['ImiH1']))
    story.append(Paragraph(essential_insight, styles['ImiInsight']))
    story.append(Spacer(1, 8*mm))

    # Scorecard
    if scorecard_data:
        story.append(Paragraph("Key Metric Performance", styles['ImiH1']))
        story.append(build_scorecard_table(scorecard_data))
        story.append(Spacer(1, 8*mm))

    # Key findings
    story.append(Paragraph("Key Findings", styles['ImiH1']))
    for i, finding in enumerate(findings, 1):
        story.append(KeepTogether([
            Paragraph(f"{i}. {finding['title']}", styles['ImiH2']),
            Paragraph(finding['body'], styles['ImiBody']),
            Paragraph(f"Base: n={finding.get('n', 'N/A')} | {finding.get('base_flag', '')}",
                      styles['ImiCaption']),
            Spacer(1, 4*mm)
        ]))

    # Recommendation
    story.append(PageBreak())
    story.append(Paragraph("IMI Recommendation", styles['ImiH1']))
    story.append(Paragraph(recommendation, styles['ImiInsight']))

    doc.build(story, onFirstPage=make_header_footer(study_name, client_name, wave),
              onLaterPages=make_header_footer(study_name, client_name, wave))

    return output_path

WeasyPrint — HTML to PDF (Simpler Path)

# pip install weasyprint
from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration

def html_to_pdf(html_content: str, output_path: str, base_url: str | None = None) -> str:
    """Convert HTML string to PDF with WeasyPrint."""
    font_config = FontConfiguration()

    # IMI brand CSS
    imi_css = CSS(string="""
        @page {
            margin: 15mm 15mm 20mm 15mm;
            @top-center { content: "IMI International — CONFIDENTIAL"; font-size: 8pt; }
            @bottom-right { content: "Page " counter(page); font-size: 8pt; }
        }
        body { font-family: Arial, Helvetica, sans-serif; font-size: 10pt; color: #333; }
        h1 { color: #1B2B4B; border-bottom: 2px solid #00A89D; padding-bottom: 4px; }
        h2 { color: #0066CC; }
        .insight-box {
            background: #F5F7FA; border-left: 4px solid #00A89D;
            padding: 12px; margin: 12px 0; font-weight: bold;
        }
        table { width: 100%; border-collapse: collapse; font-size: 9pt; }
        th { background: #1B2B4B; color: white; padding: 6px; }
        td { padding: 5px; border: 1px solid #DDD; }
        tr:nth-child(even) { background: #F5F7FA; }
        .above-norm { color: #00A89D; font-weight: bold; }
        .below-norm { color: #CC3300; font-weight: bold; }
        .directional { color: #888; font-style: italic; }
    """, font_config=font_config)

    HTML(string=html_content, base_url=base_url).write_pdf(
        output_path,
        stylesheets=[imi_css],
        font_config=font_config
    )
    return output_path

Quick Reference: PDF Generation Decision Tree

Need a PDF? Start here:
├── Is the content data-heavy (tables, charts, scorecards)?
│   → ReportLab Platypus (full control)
├── Is the content text-heavy (report, proposal, topline prose)?
│   → WeasyPrint (write HTML → convert) — faster to develop
├── Is there an existing HTML/Jinja2 template?
│   → WeasyPrint — drop-in
├── Need pixel-precise brand control?
│   → ReportLab Canvas (lower level but maximum control)
└── Merging/splitting existing PDFs?
    → pypdf (see pdf skill)

*See also: python-data-engine (data for the reports), pptx-engine (slide decks), excel-automation (spreadsheet exports)*

AXE MCP Server Integration

Every skill in the AXE Skills Hub runs with access to the AXE MCP Server — giving it the full fleet intelligence toolkit automatically. No setup required; tools are available in any AXE-powered session.

Core Tools Available

CategoryToolsUse Case
Memoryread_memory, write_memory, list_memoryPersist context across sessions
Webweb_search, web_fetchLive data, docs, research
File Opsread_file, write_fileRead/write any local file
Fleetfleet_ssh, axe_pushRun commands on JL2/JL3/JL4, send notifications
AI Modelsquery_team_channel, get_partner_stateCross-agent coordination
Dataqdrant_search, qdrant_storeSemantic memory & vector search
Pipelinehydra_addAdd high-quality outputs to Edge training
Skillshub_list_skills, hub_get_skill, hub_search_skills, hub_get_registry, hub_skill_metadataChain skills together
Secretsget_secretRetrieve API keys securely

Quick Start

# In any AXE session, tools are pre-loaded. Example chaining:

# 1. Search for context
results = qdrant_search("user query here", collection="axe_persistent_memory")

# 2. Fetch live data if needed
content = web_fetch("https://docs.example.com/api")

# 3. Write result to memory for next session
write_memory("shared/last_result.md", output)

# 4. Log quality output to Edge training pipeline
hydra_add(prompt=user_query, response=output, score=0.9, source="skill-name")

Edge Training Integration

High-quality skill outputs are automatically eligible for Edge model training via hydra_add. When a response scores ≥0.85 in evals, pipe it to the Hydra pipeline to compound Edge's knowledge. This is how skills make Edge smarter over time.

# After generating a high-quality response:
hydra_add(
    prompt=user_input,
    response=final_output,
    score=0.9,          # eval score
    source="skill-name" # tracks provenance
)

Metadata

Category
Document
Tier
community
Version
1.0.0
License
MIT
Path
skills/reportlab-pdf-factory/SKILL.md

Use with an agent

Fetch this skill’s definition over the open API — no key required.

curl -s /v1/skills/reportlab-pdf-factory

View source ↗