mgbam commited on
Commit
eb87d5f
·
verified ·
1 Parent(s): ea3fdd7

Update report_utils.py

Browse files
Files changed (1) hide show
  1. report_utils.py +51 -59
report_utils.py CHANGED
@@ -1,9 +1,7 @@
1
- # report_utils.py (Example filename)
2
-
3
  import os
4
  import logging
5
  import tempfile
6
- from fpdf import FPDF # Recommend using fpdf2: pip install fpdf2
7
  from PIL import Image
8
  from typing import Optional, Dict, Any
9
 
@@ -11,16 +9,19 @@ logger = logging.getLogger(__name__)
11
 
12
  # Define constants for styling or fixed text
13
  REPORT_TITLE = "MediVision QA - AI Analysis Report"
14
- FOOTER_TEXT = "Disclaimer: AI-generated analysis. For informational purposes only. Requires expert clinical validation."
15
- ERROR_COLOR = (255, 0, 0) # Red for errors in PDF
16
- DEFAULT_COLOR = (0, 0, 0) # Black
 
 
 
17
  TEMP_IMAGE_PREFIX = "medivision_report_img_"
18
 
19
  def generate_pdf_report_bytes(
20
  session_id: str,
21
- image: Optional[Image.Image], # Allow None explicitly
22
  analysis_outputs: Dict[str, str]
23
- ) -> Optional[bytes]:
24
  """
25
  Generates a PDF report summarizing the analysis session.
26
 
@@ -31,8 +32,8 @@ def generate_pdf_report_bytes(
31
  session_id: The unique identifier for the analysis session.
32
  image: A PIL Image object to embed in the report. If None, a
33
  placeholder message is added.
34
- analysis_outputs: A dictionary where keys are section titles (str)
35
- and values are the content (str) for the report.
36
 
37
  Returns:
38
  The generated PDF as bytes if successful, otherwise None.
@@ -41,7 +42,7 @@ def generate_pdf_report_bytes(
41
 
42
  pdf = FPDF()
43
  pdf.add_page()
44
- pdf.set_margins(15, 15, 15) # Set consistent margins
45
  pdf.set_auto_page_break(auto=True, margin=15)
46
 
47
  # --- Header ---
@@ -49,120 +50,111 @@ def generate_pdf_report_bytes(
49
  pdf.cell(0, 10, txt=REPORT_TITLE, ln=True, align='C')
50
  pdf.set_font("Arial", size=10)
51
  pdf.cell(0, 8, txt=f"Session ID: {session_id}", ln=True, align='C')
52
- pdf.ln(10) # Space after header
53
 
54
  # --- Embed Image ---
55
- temp_image_path = None # Define path variable outside try
56
  try:
57
  if image is None:
58
- logger.warning("No image provided for PDF report.")
59
- pdf.set_font("Arial", 'I', 10)
60
- pdf.set_text_color(*ERROR_COLOR)
61
- pdf.cell(0, 10, "[No image available for this report]", ln=True, align='C')
62
- pdf.set_text_color(*DEFAULT_COLOR)
63
- pdf.ln(5)
64
  else:
65
- # Save image temporarily to embed it by path (most compatible fpdf method)
66
- # Using delete=False requires manual cleanup in 'finally'
67
  with tempfile.NamedTemporaryFile(delete=False, suffix=".png", prefix=TEMP_IMAGE_PREFIX) as tmpfile:
68
  temp_image_path = tmpfile.name
69
  logger.debug(f"Saving temporary image for PDF report to: {temp_image_path}")
70
  image.save(temp_image_path, format="PNG")
71
- # tmpfile is closed here, but file still exists due to delete=False
72
-
73
- # Calculate image display size to fit page width while maintaining aspect ratio
74
- page_width_mm = pdf.w - pdf.l_margin - pdf.r_margin # Usable page width
75
- max_img_width_mm = page_width_mm * 0.9 # Allow some padding
76
 
77
  img_width_px, img_height_px = image.size
78
  if img_width_px <= 0 or img_height_px <= 0:
79
- raise ValueError(f"Invalid image dimensions: {image.size}")
80
 
81
  aspect_ratio = img_height_px / img_width_px
82
- display_width_mm = min(max_img_width_mm, img_width_px / 3) # Basic heuristic for pixel to mm, adjust if needed
 
83
  display_height_mm = display_width_mm * aspect_ratio
84
 
85
- # Ensure image doesn't overflow page height
86
- available_height_mm = pdf.h - pdf.get_y() - pdf.b_margin - 10 # Available vertical space minus padding
87
  if display_height_mm > available_height_mm:
88
- logger.debug(f"Image height ({display_height_mm:.1f}mm) exceeds available space ({available_height_mm:.1f}mm), resizing.")
89
  display_height_mm = available_height_mm
90
- display_width_mm = display_height_mm / aspect_ratio # Recalculate width based on new height
91
 
92
- # Center the image horizontally
93
  x_pos = (pdf.w - display_width_mm) / 2
94
- logger.debug(f"Embedding image: Path='{os.path.basename(temp_image_path)}', Pos=({x_pos:.1f}, {pdf.get_y():.1f}), Size=({display_width_mm:.1f}x{display_height_mm:.1f} mm)")
95
  pdf.image(temp_image_path, x=x_pos, y=pdf.get_y(), w=display_width_mm, h=display_height_mm)
96
- pdf.ln(display_height_mm + 5) # Move cursor below image plus padding
97
 
98
  except Exception as e:
99
  err_msg = f"Error processing or embedding image in PDF: {e}"
100
  logger.error(err_msg, exc_info=True)
101
- # Add an error message within the PDF itself
102
  pdf.set_text_color(*ERROR_COLOR)
103
  pdf.set_font("Arial", 'B', 10)
104
  pdf.multi_cell(0, 6, "[Error displaying image in report - See application logs for details]", align='C')
105
  pdf.set_text_color(*DEFAULT_COLOR)
106
- pdf.set_font("Arial", size=10) # Reset font
107
  pdf.ln(5)
108
  finally:
109
- # --- Crucial Cleanup for delete=False ---
110
  if temp_image_path and os.path.exists(temp_image_path):
111
  try:
112
  os.remove(temp_image_path)
113
- logger.debug(f"Successfully removed temporary image file: {temp_image_path}")
114
  except OSError as e:
115
- # Log warning but don't crash PDF generation if removal fails
116
  logger.warning(f"Could not remove temporary image file '{temp_image_path}': {e}")
117
 
118
- # --- Add Analysis Sections ---
119
  pdf.set_font("Arial", 'B', 12)
120
  pdf.cell(0, 10, txt="Analysis Results", ln=True)
121
 
122
  if not analysis_outputs:
123
- pdf.set_font("Arial", 'I', 10)
124
- pdf.cell(0, 6, txt="No analysis results were generated for this session.", ln=True)
125
- pdf.ln(4)
126
  else:
127
  for section_title, content in analysis_outputs.items():
128
  # Section Title
129
  pdf.set_font("Arial", 'B', 11)
130
- # Use multi_cell for title in case it's long
131
  pdf.multi_cell(0, 6, f"{section_title}:")
132
  pdf.set_font("Arial", size=10)
133
 
134
- # Section Content
135
  content_to_write = content.strip() if content else "N/A"
136
  try:
137
- # FPDF(2) primarily supports Latin-1. Encode/decode with 'replace'
138
- # handles unsupported characters gracefully for PDF generation.
139
  encoded_content = content_to_write.encode('latin-1', 'replace').decode('latin-1')
140
  pdf.multi_cell(0, 5, txt=encoded_content)
141
  except Exception as e:
142
- # Handle potential errors during string processing or writing
143
  logger.error(f"Error writing section '{section_title}' to PDF: {e}", exc_info=True)
144
  pdf.set_text_color(*ERROR_COLOR)
145
- pdf.multi_cell(0, 5, f"[Error rendering content for this section - See application logs]")
146
  pdf.set_text_color(*DEFAULT_COLOR)
147
 
148
- pdf.ln(4) # Space between sections
149
 
150
  # --- Footer Disclaimer ---
151
- pdf.set_y(-20) # Position footer slightly higher
152
  pdf.set_font("Arial", 'I', 8)
153
- pdf.set_text_color(128, 128, 128) # Gray color for footer
154
- pdf.multi_cell(0, 4, txt=FOOTER_TEXT, align='C') # Use multi_cell for potential wrapping
155
- pdf.set_text_color(*DEFAULT_COLOR) # Reset color
156
 
157
  # --- Output PDF as Bytes ---
158
  try:
159
- # Output 'S' returns bytes (implicitly latin-1 encoded string first)
160
  pdf_bytes = pdf.output(dest='S')
161
  if isinstance(pdf_bytes, str):
162
- # If fpdf returned str (older versions?), encode explicitly
163
- pdf_bytes = pdf_bytes.encode('latin-1')
164
  logger.info(f"PDF report ({len(pdf_bytes)} bytes) generated successfully for session {session_id}.")
165
  return pdf_bytes
166
  except Exception as e:
167
  logger.error(f"Critical error during final PDF byte generation: {e}", exc_info=True)
168
- return None # Indicate failure
 
 
 
1
  import os
2
  import logging
3
  import tempfile
4
+ from fpdf import FPDF # Recommend using fpdf2: pip install fpdf2
5
  from PIL import Image
6
  from typing import Optional, Dict, Any
7
 
 
9
 
10
  # Define constants for styling or fixed text
11
  REPORT_TITLE = "MediVision QA - AI Analysis Report"
12
+ FOOTER_TEXT = (
13
+ "Disclaimer: AI-generated analysis. For informational purposes only. "
14
+ "Requires expert clinical validation."
15
+ )
16
+ ERROR_COLOR = (255, 0, 0) # Red for errors in PDF
17
+ DEFAULT_COLOR = (0, 0, 0) # Black
18
  TEMP_IMAGE_PREFIX = "medivision_report_img_"
19
 
20
  def generate_pdf_report_bytes(
21
  session_id: str,
22
+ image: Optional[Image.Image],
23
  analysis_outputs: Dict[str, str]
24
+ ) -> Optional[bytes]:
25
  """
26
  Generates a PDF report summarizing the analysis session.
27
 
 
32
  session_id: The unique identifier for the analysis session.
33
  image: A PIL Image object to embed in the report. If None, a
34
  placeholder message is added.
35
+ analysis_outputs: A dictionary with section titles as keys and
36
+ content strings as values.
37
 
38
  Returns:
39
  The generated PDF as bytes if successful, otherwise None.
 
42
 
43
  pdf = FPDF()
44
  pdf.add_page()
45
+ pdf.set_margins(15, 15, 15)
46
  pdf.set_auto_page_break(auto=True, margin=15)
47
 
48
  # --- Header ---
 
50
  pdf.cell(0, 10, txt=REPORT_TITLE, ln=True, align='C')
51
  pdf.set_font("Arial", size=10)
52
  pdf.cell(0, 8, txt=f"Session ID: {session_id}", ln=True, align='C')
53
+ pdf.ln(10) # Space after header
54
 
55
  # --- Embed Image ---
56
+ temp_image_path = None
57
  try:
58
  if image is None:
59
+ logger.warning("No image provided for PDF report.")
60
+ pdf.set_font("Arial", 'I', 10)
61
+ pdf.set_text_color(*ERROR_COLOR)
62
+ pdf.cell(0, 10, "[No image available for this report]", ln=True, align='C')
63
+ pdf.set_text_color(*DEFAULT_COLOR)
64
+ pdf.ln(5)
65
  else:
66
+ # Save the image temporarily to a file for embedding
 
67
  with tempfile.NamedTemporaryFile(delete=False, suffix=".png", prefix=TEMP_IMAGE_PREFIX) as tmpfile:
68
  temp_image_path = tmpfile.name
69
  logger.debug(f"Saving temporary image for PDF report to: {temp_image_path}")
70
  image.save(temp_image_path, format="PNG")
71
+
72
+ # Calculate display size: fit within 90% of page width
73
+ page_width_mm = pdf.w - pdf.l_margin - pdf.r_margin
74
+ max_img_width_mm = page_width_mm * 0.9
 
75
 
76
  img_width_px, img_height_px = image.size
77
  if img_width_px <= 0 or img_height_px <= 0:
78
+ raise ValueError(f"Invalid image dimensions: {image.size}")
79
 
80
  aspect_ratio = img_height_px / img_width_px
81
+ # Use a basic heuristic: convert pixel dimensions to mm (adjustable)
82
+ display_width_mm = min(max_img_width_mm, img_width_px / 3)
83
  display_height_mm = display_width_mm * aspect_ratio
84
 
85
+ # Ensure the image fits vertically on the page
86
+ available_height_mm = pdf.h - pdf.get_y() - pdf.b_margin - 10
87
  if display_height_mm > available_height_mm:
88
+ logger.debug(f"Image height ({display_height_mm:.1f}mm) exceeds available space ({available_height_mm:.1f}mm). Resizing.")
89
  display_height_mm = available_height_mm
90
+ display_width_mm = display_height_mm / aspect_ratio
91
 
92
+ # Center image horizontally
93
  x_pos = (pdf.w - display_width_mm) / 2
94
+ 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)")
95
  pdf.image(temp_image_path, x=x_pos, y=pdf.get_y(), w=display_width_mm, h=display_height_mm)
96
+ pdf.ln(display_height_mm + 5)
97
 
98
  except Exception as e:
99
  err_msg = f"Error processing or embedding image in PDF: {e}"
100
  logger.error(err_msg, exc_info=True)
 
101
  pdf.set_text_color(*ERROR_COLOR)
102
  pdf.set_font("Arial", 'B', 10)
103
  pdf.multi_cell(0, 6, "[Error displaying image in report - See application logs for details]", align='C')
104
  pdf.set_text_color(*DEFAULT_COLOR)
105
+ pdf.set_font("Arial", size=10)
106
  pdf.ln(5)
107
  finally:
108
+ # Ensure temporary image file is removed
109
  if temp_image_path and os.path.exists(temp_image_path):
110
  try:
111
  os.remove(temp_image_path)
112
+ logger.debug(f"Removed temporary image file: {temp_image_path}")
113
  except OSError as e:
 
114
  logger.warning(f"Could not remove temporary image file '{temp_image_path}': {e}")
115
 
116
+ # --- Analysis Sections ---
117
  pdf.set_font("Arial", 'B', 12)
118
  pdf.cell(0, 10, txt="Analysis Results", ln=True)
119
 
120
  if not analysis_outputs:
121
+ pdf.set_font("Arial", 'I', 10)
122
+ pdf.cell(0, 6, "No analysis results were generated for this session.", ln=True)
123
+ pdf.ln(4)
124
  else:
125
  for section_title, content in analysis_outputs.items():
126
  # Section Title
127
  pdf.set_font("Arial", 'B', 11)
 
128
  pdf.multi_cell(0, 6, f"{section_title}:")
129
  pdf.set_font("Arial", size=10)
130
 
 
131
  content_to_write = content.strip() if content else "N/A"
132
  try:
133
+ # Encode content using Latin-1 with replacement for unsupported characters
 
134
  encoded_content = content_to_write.encode('latin-1', 'replace').decode('latin-1')
135
  pdf.multi_cell(0, 5, txt=encoded_content)
136
  except Exception as e:
 
137
  logger.error(f"Error writing section '{section_title}' to PDF: {e}", exc_info=True)
138
  pdf.set_text_color(*ERROR_COLOR)
139
+ pdf.multi_cell(0, 5, "[Error rendering content for this section - See application logs]")
140
  pdf.set_text_color(*DEFAULT_COLOR)
141
 
142
+ pdf.ln(4) # Space between sections
143
 
144
  # --- Footer Disclaimer ---
145
+ pdf.set_y(-20) # Position footer near the bottom
146
  pdf.set_font("Arial", 'I', 8)
147
+ pdf.set_text_color(128, 128, 128) # Gray footer text
148
+ pdf.multi_cell(0, 4, txt=FOOTER_TEXT, align='C')
149
+ pdf.set_text_color(*DEFAULT_COLOR) # Reset text color
150
 
151
  # --- Output PDF as Bytes ---
152
  try:
 
153
  pdf_bytes = pdf.output(dest='S')
154
  if isinstance(pdf_bytes, str):
155
+ pdf_bytes = pdf_bytes.encode('latin-1')
 
156
  logger.info(f"PDF report ({len(pdf_bytes)} bytes) generated successfully for session {session_id}.")
157
  return pdf_bytes
158
  except Exception as e:
159
  logger.error(f"Critical error during final PDF byte generation: {e}", exc_info=True)
160
+ return None