PDF Debug Skill
This project generates PDFs using WeasyPrint + Jinja2 templates. The pipeline is:
TrendingTopicsReport (Pydantic model)
→ _render_html() # Jinja2: src/campaign_ai/templates/report.html.j2
→ HTML(string=...).write_pdf() # WeasyPrint
Key files:
src/campaign_ai/pdf_generator.py — PDF rendering logic
src/campaign_ai/templates/report.html.j2 — Jinja2 HTML template
src/campaign_ai/models.py — TrendingTopicsReport and MarketingCampaign models
Step 1 — Check system dependencies
WeasyPrint requires native libs. On Ubuntu/Debian:
dpkg -l | grep -E "libpango|libcairo|libgdk-pixbuf|fonts-"
If any are missing, install them:
sudo apt-get install -y libpango-1.0-0 libpangoft2-1.0-0 libgdk-pixbuf-2.0-0 libffi-dev libcairo2 fonts-liberation
On macOS (Apple Silicon), WeasyPrint needs Homebrew libs at /opt/homebrew/lib. The
_configure_weasyprint_library_path() function in pdf_generator.py patches
DYLD_FALLBACK_LIBRARY_PATH automatically — verify it's being called before import.
Step 2 — Render HTML in isolation
Before involving WeasyPrint, check that the Jinja2 template renders correctly with mock data:
# Run with: uv run python -c "..."
from campaign_ai.models import TrendingTopicsReport, MarketingCampaign
from campaign_ai.pdf_generator import _render_html
from campaign_ai.config import Settings
mock = TrendingTopicsReport(
keyword="test",
campaigns=[
MarketingCampaign(
topic=f"Topic {i}",
trend_summary="This topic is trending because of X, Y, and Z factors in the market.",
company_relevance="This trend aligns well with the company's core brand values.",
campaign_concept="A bold campaign idea that connects consumers to the brand.",
tagline="Connect Every Moment",
channels=["Social Media", "OOH", "Partnerships"],
execution_steps=["Step 1", "Step 2", "Step 3", "Step 4"],
)
for i in range(1, 4)
],
)
settings = Settings()
html = _render_html(mock, settings)
with open("/tmp/debug_report.html", "w") as f:
f.write(html)
print("HTML written to /tmp/debug_report.html")
Open /tmp/debug_report.html in a browser to visually inspect the template output.
Step 3 — Render a test PDF end-to-end
# Run with: uv run python -c "..."
from pathlib import Path
from campaign_ai.models import TrendingTopicsReport, MarketingCampaign
from campaign_ai.pdf_generator import render_pdf
from campaign_ai.config import Settings
mock = TrendingTopicsReport(
keyword="test",
campaigns=[
MarketingCampaign(
topic=f"Topic {i}",
trend_summary="This topic is trending because of X, Y, and Z factors in the market.",
company_relevance="This trend aligns well with the company's core brand values.",
campaign_concept="A bold campaign idea that connects consumers to the brand.",
tagline="Connect Every Moment",
channels=["Social Media", "OOH", "Partnerships"],
execution_steps=["Step 1", "Step 2", "Step 3", "Step 4"],
)
for i in range(1, 4)
],
)
settings = Settings()
render_pdf(mock, Path("/tmp/debug_report.pdf"), settings)
print("PDF written to /tmp/debug_report.pdf")
Step 4 — Diagnose WeasyPrint import errors
If WeasyPrint fails to import or raises an OSError: cannot load library:
uv run python -c "import weasyprint; print(weasyprint.__version__)"
Common fixes:
- Linux: install missing system libs (Step 1)
- macOS: ensure
/opt/homebrew/lib exists and Homebrew packages pango, gdk-pixbuf, libffi are installed via brew install pango gdk-pixbuf libffi
- Font issues: WeasyPrint fetches Google Fonts over the network (
fonts.googleapis.com). In offline/CI environments, the template may fall back to Arial — this is expected behaviour.
Step 5 — Check template variable names
The template at src/campaign_ai/templates/report.html.j2 expects:
report — a TrendingTopicsReport instance
report.campaigns — list of exactly 3 MarketingCampaign objects
- Per campaign:
topic, trend_summary, company_relevance, campaign_concept, tagline, channels (list), execution_steps (list)
If a jinja2.UndefinedError occurs, verify field names match those in src/campaign_ai/models.py.