From e0cbbab312ad1f9dd7ca26fa0c66b071caad12e3 Mon Sep 17 00:00:00 2001 From: Francis Secada Date: Wed, 4 Feb 2026 14:43:04 -0500 Subject: [PATCH] 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 --- src/backend/core/pdf_service.py | 10 ++-------- src/backend/site/router.py | 8 ++++++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/backend/core/pdf_service.py b/src/backend/core/pdf_service.py index fc46795..190e9e9 100644 --- a/src/backend/core/pdf_service.py +++ b/src/backend/core/pdf_service.py @@ -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( diff --git a/src/backend/site/router.py b/src/backend/site/router.py index 4e4bd67..204f16f 100644 --- a/src/backend/site/router.py +++ b/src/backend/site/router.py @@ -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}",