fix: resolve critical PDF generation bugs

Fixed two critical errors preventing PDF downloads:

1. KeyError: "Style 'BodyText' already defined in stylesheet"
   - Renamed custom style from "BodyText" to "ReportBodyText"
   - BodyText is a reserved ReportLab built-in style name
   - Updated all references to use new name

2. AttributeError: 'int' object has no attribute 'encode'
   - StreamingResponse expected an iterator, not BytesIO directly
   - Added iterfile() generator function to yield BytesIO chunks
   - Properly resets buffer position with seek(0) before iteration

Additional:
- Removed unused hex_to_rgb_tuple() function (dead code cleanup)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 14:43:04 -05:00
parent ce76f958f7
commit e0cbbab312
2 changed files with 8 additions and 10 deletions

View File

@ -38,12 +38,6 @@ NEUTRAL_100 = colors.HexColor("#F3F4F6")
WHITE = colors.white
def hex_to_rgb_tuple(hex_color: str) -> tuple[float, float, float]:
"""Convert hex color to RGB tuple (0-1 range)"""
hex_color = hex_color.lstrip("#")
return tuple(int(hex_color[i : i + 2], 16) / 255 for i in (0, 2, 4))
def compute_content_hash(analysis: SwotAnalysis) -> str:
"""
Compute SHA-256 hash of SWOT analysis content for caching.
@ -124,7 +118,7 @@ class SwotPDFGenerator:
# Body text style
self.styles.add(
ParagraphStyle(
name="BodyText",
name="ReportBodyText",
parent=self.styles["Normal"],
fontSize=11,
textColor=NEUTRAL_700,
@ -243,7 +237,7 @@ class SwotPDFGenerator:
# Wrap summary in a table for better styling
summary_para = Paragraph(
self.analysis.analysis, self.styles["BodyText"]
self.analysis.analysis, self.styles["ReportBodyText"]
)
summary_table = Table([[summary_para]], colWidths=[6.5 * inch])
summary_table.setStyle(

View File

@ -227,9 +227,13 @@ async def download_pdf(request: Request) -> StreamingResponse:
# Prepare filename
filename = f"swot-analysis-{session_id[:8]}.pdf"
# Return as streaming response
# Return as streaming response (iterate over BytesIO in chunks)
def iterfile():
pdf_buffer.seek(0)
yield from pdf_buffer
return StreamingResponse(
content=pdf_buffer,
content=iterfile(),
media_type="application/pdf",
headers={
"Content-Disposition": f"attachment; filename={filename}",