File size: 6,946 Bytes
7ef2f88
 
 
f40d958
45180f8
f40d958
7ef2f88
 
 
f40d958
 
eb87d5f
f40d958
 
eb87d5f
f40d958
 
 
7ef2f88
 
 
eb87d5f
f40d958
eb87d5f
45180f8
f40d958
45180f8
f40d958
 
45180f8
 
 
f40d958
 
 
 
45180f8
 
 
 
 
 
7ef2f88
 
f40d958
 
45180f8
f40d958
 
45180f8
6e51089
 
f40d958
6e51089
45180f8
eb87d5f
6e51089
45180f8
eb87d5f
 
f40d958
 
 
eb87d5f
45180f8
f40d958
45180f8
 
f40d958
 
 
 
eb87d5f
f40d958
45180f8
 
 
f40d958
45180f8
 
f40d958
 
45180f8
 
f40d958
 
 
eb87d5f
45180f8
eb87d5f
45180f8
eb87d5f
45180f8
f40d958
45180f8
f40d958
6e51089
 
45180f8
6e51089
f40d958
45180f8
f40d958
 
eb87d5f
45180f8
6e51089
f40d958
6e51089
 
 
eb87d5f
f40d958
 
7ef2f88
f40d958
7ef2f88
f40d958
6e51089
45180f8
eb87d5f
f40d958
eb87d5f
45180f8
 
f40d958
45180f8
f40d958
45180f8
 
f40d958
45180f8
f40d958
45180f8
f40d958
 
 
 
 
 
45180f8
f40d958
7ef2f88
f40d958
 
6e51089
f40d958
eb87d5f
f40d958
7ef2f88
f40d958
7ef2f88
45180f8
 
f40d958
45180f8
7ef2f88
f40d958
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import os
import logging
import tempfile
from fpdf import FPDF  # Recommend using fpdf2: pip install fpdf2
from PIL import Image
from typing import Optional, Dict, Any

logger = logging.getLogger(__name__)

# Define constants for styling or fixed text
REPORT_TITLE = "MediVision QA - AI Analysis Report"
FOOTER_TEXT = (
    "Disclaimer: AI-generated analysis. For informational purposes only. "
    "Requires expert clinical validation."
)
ERROR_COLOR = (255, 0, 0)  # Red for errors in PDF
DEFAULT_COLOR = (0, 0, 0)  # Black
TEMP_IMAGE_PREFIX = "medivision_report_img_"

def generate_pdf_report_bytes(
    session_id: str,
    image: Optional[Image.Image],
    analysis_outputs: Dict[str, str]
) -> Optional[bytes]:
    """
    Generates a PDF report summarizing the analysis session.

    Includes session ID, an embedded image (if provided), and formatted
    analysis outputs. Handles errors during generation gracefully.

    Args:
        session_id: The unique identifier for the analysis session.
        image: A PIL Image object to embed in the report. If None, a
               placeholder message is added.
        analysis_outputs: A dictionary with section titles as keys and
                          content strings as values.

    Returns:
        The generated PDF as bytes if successful, otherwise None.
    """
    logger.info(f"Starting PDF report generation for session ID: {session_id}")

    pdf = FPDF()
    pdf.add_page()
    pdf.set_margins(15, 15, 15)
    pdf.set_auto_page_break(auto=True, margin=15)

    # --- Header ---
    pdf.set_font("Arial", 'B', 16)
    pdf.cell(0, 10, txt=REPORT_TITLE, ln=True, align='C')
    pdf.set_font("Arial", size=10)
    pdf.cell(0, 8, txt=f"Session ID: {session_id}", ln=True, align='C')
    pdf.ln(10)  # Space after header

    # --- Embed Image ---
    temp_image_path = None
    try:
        if image is None:
            logger.warning("No image provided for PDF report.")
            pdf.set_font("Arial", 'I', 10)
            pdf.set_text_color(*ERROR_COLOR)
            pdf.cell(0, 10, "[No image available for this report]", ln=True, align='C')
            pdf.set_text_color(*DEFAULT_COLOR)
            pdf.ln(5)
        else:
            # Save the image temporarily to a file for embedding
            with tempfile.NamedTemporaryFile(delete=False, suffix=".png", prefix=TEMP_IMAGE_PREFIX) as tmpfile:
                temp_image_path = tmpfile.name
                logger.debug(f"Saving temporary image for PDF report to: {temp_image_path}")
                image.save(temp_image_path, format="PNG")
            
            # Calculate display size: fit within 90% of page width
            page_width_mm = pdf.w - pdf.l_margin - pdf.r_margin
            max_img_width_mm = page_width_mm * 0.9

            img_width_px, img_height_px = image.size
            if img_width_px <= 0 or img_height_px <= 0:
                raise ValueError(f"Invalid image dimensions: {image.size}")

            aspect_ratio = img_height_px / img_width_px
            # Use a basic heuristic: convert pixel dimensions to mm (adjustable)
            display_width_mm = min(max_img_width_mm, img_width_px / 3)
            display_height_mm = display_width_mm * aspect_ratio

            # Ensure the image fits vertically on the page
            available_height_mm = pdf.h - pdf.get_y() - pdf.b_margin - 10
            if display_height_mm > available_height_mm:
                logger.debug(f"Image height ({display_height_mm:.1f}mm) exceeds available space ({available_height_mm:.1f}mm). Resizing.")
                display_height_mm = available_height_mm
                display_width_mm = display_height_mm / aspect_ratio

            # Center image horizontally
            x_pos = (pdf.w - display_width_mm) / 2
            logger.debug(f"Embedding image: {os.path.basename(temp_image_path)} at position ({x_pos:.1f}, {pdf.get_y():.1f}) with size ({display_width_mm:.1f}x{display_height_mm:.1f} mm)")
            pdf.image(temp_image_path, x=x_pos, y=pdf.get_y(), w=display_width_mm, h=display_height_mm)
            pdf.ln(display_height_mm + 5)

    except Exception as e:
        err_msg = f"Error processing or embedding image in PDF: {e}"
        logger.error(err_msg, exc_info=True)
        pdf.set_text_color(*ERROR_COLOR)
        pdf.set_font("Arial", 'B', 10)
        pdf.multi_cell(0, 6, "[Error displaying image in report - See application logs for details]", align='C')
        pdf.set_text_color(*DEFAULT_COLOR)
        pdf.set_font("Arial", size=10)
        pdf.ln(5)
    finally:
        # Ensure temporary image file is removed
        if temp_image_path and os.path.exists(temp_image_path):
            try:
                os.remove(temp_image_path)
                logger.debug(f"Removed temporary image file: {temp_image_path}")
            except OSError as e:
                logger.warning(f"Could not remove temporary image file '{temp_image_path}': {e}")

    # --- Analysis Sections ---
    pdf.set_font("Arial", 'B', 12)
    pdf.cell(0, 10, txt="Analysis Results", ln=True)

    if not analysis_outputs:
        pdf.set_font("Arial", 'I', 10)
        pdf.cell(0, 6, "No analysis results were generated for this session.", ln=True)
        pdf.ln(4)
    else:
        for section_title, content in analysis_outputs.items():
            # Section Title
            pdf.set_font("Arial", 'B', 11)
            pdf.multi_cell(0, 6, f"{section_title}:")
            pdf.set_font("Arial", size=10)

            content_to_write = content.strip() if content else "N/A"
            try:
                # Encode content using Latin-1 with replacement for unsupported characters
                encoded_content = content_to_write.encode('latin-1', 'replace').decode('latin-1')
                pdf.multi_cell(0, 5, txt=encoded_content)
            except Exception as e:
                logger.error(f"Error writing section '{section_title}' to PDF: {e}", exc_info=True)
                pdf.set_text_color(*ERROR_COLOR)
                pdf.multi_cell(0, 5, "[Error rendering content for this section - See application logs]")
                pdf.set_text_color(*DEFAULT_COLOR)

            pdf.ln(4)  # Space between sections

    # --- Footer Disclaimer ---
    pdf.set_y(-20)  # Position footer near the bottom
    pdf.set_font("Arial", 'I', 8)
    pdf.set_text_color(128, 128, 128)  # Gray footer text
    pdf.multi_cell(0, 4, txt=FOOTER_TEXT, align='C')
    pdf.set_text_color(*DEFAULT_COLOR)  # Reset text color

    # --- Output PDF as Bytes ---
    try:
        pdf_bytes = pdf.output(dest='S')
        if isinstance(pdf_bytes, str):
            pdf_bytes = pdf_bytes.encode('latin-1')
        logger.info(f"PDF report ({len(pdf_bytes)} bytes) generated successfully for session {session_id}.")
        return pdf_bytes
    except Exception as e:
        logger.error(f"Critical error during final PDF byte generation: {e}", exc_info=True)
        return None