| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- """
- PDF Exporter
- Handles PDF report generation and export functionality using ReportLab.
- """
- import os
- import time
- import tempfile
- from datetime import datetime
- from typing import Dict, List, Tuple, Optional
- from PyQt5.QtWidgets import QMessageBox, QFileDialog
- from PyQt5.QtGui import QPixmap
- try:
- from reportlab.lib import colors
- from reportlab.lib.pagesizes import A4
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
- from reportlab.lib.units import inch
- from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage, PageBreak
- from reportlab.lib.enums import TA_CENTER, TA_LEFT
- HAS_REPORTLAB = True
- except ImportError:
- HAS_REPORTLAB = False
- class PDFExporter:
- """
- Handles PDF report generation and export.
-
- Attributes:
- has_reportlab: Boolean indicating if ReportLab is available
- """
-
- def __init__(self):
- """Initialize the PDF exporter."""
- self.has_reportlab = HAS_REPORTLAB
-
- def export_report(
- self,
- parent_widget,
- report_id: str,
- report_content: Dict,
- visualizations: List[Tuple[str, QPixmap]],
- include_visualizations: bool = True
- ) -> bool:
- """
- Export report as PDF.
-
- Args:
- parent_widget: Parent widget for dialogs
- report_id: Report ID for filename
- report_content: Dictionary with report text content
- visualizations: List of (title, QPixmap) tuples
- include_visualizations: Whether to include visualizations
-
- Returns:
- bool: True if export succeeded, False otherwise
- """
- if not self.has_reportlab:
- QMessageBox.critical(
- parent_widget,
- "Library Not Available",
- "ReportLab is not installed. Please install it to use PDF export:\n\n"
- "pip install reportlab"
- )
- return False
-
- # Get file path from user
- file_path, _ = QFileDialog.getSaveFileName(
- parent_widget,
- "Save PDF Report",
- f"Report_{report_id or datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
- "PDF Files (*.pdf);;All Files (*.*)",
- options=QFileDialog.DontUseNativeDialog
- )
-
- if not file_path:
- return False # User cancelled
-
- try:
- # Create PDF document
- doc = SimpleDocTemplate(file_path, pagesize=A4)
- story = []
-
- # Add title
- styles = getSampleStyleSheet()
- title_style = ParagraphStyle(
- 'CustomTitle',
- parent=styles['Heading1'],
- fontSize=18,
- textColor=colors.HexColor('#2c3e50'),
- spaceAfter=20,
- alignment=TA_CENTER
- )
- story.append(Paragraph("Durian Analysis Report", title_style))
- story.append(Spacer(1, 0.2*inch))
-
- # Report Information
- heading_style = ParagraphStyle(
- 'CustomHeading',
- parent=styles['Heading2'],
- fontSize=12,
- textColor=colors.HexColor('#34495e'),
- spaceAfter=10
- )
- story.append(Paragraph("Report Information", heading_style))
-
- info_data = [
- ['Report ID:', report_content['report_id']],
- ['Generated:', report_content['generated']]
- ]
- info_table = Table(info_data, colWidths=[2*inch, 3.5*inch])
- info_table.setStyle(TableStyle([
- ('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#ecf0f1')),
- ('TEXTCOLOR', (0, 0), (-1, -1), colors.black),
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
- ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
- ('FONTSIZE', (0, 0), (-1, -1), 10),
- ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
- ('TOPPADDING', (0, 0), (-1, -1), 8),
- ('GRID', (0, 0), (-1, -1), 1, colors.grey)
- ]))
- story.append(info_table)
- story.append(Spacer(1, 0.3*inch))
-
- # Analysis Results
- story.append(Paragraph("Analysis Results", heading_style))
-
- results = report_content['results']
- results_data = [['Metric', 'Value']]
-
- if results.get('locule_count') is not None:
- results_data.append(['Locule Count', f"{results['locule_count']} locules"])
-
- if results.get('defect_status'):
- results_data.append(['Defect Status', f"{results['defect_status']} ({results.get('total_detections', 0)} detections)"])
-
- if results.get('shape_class'):
- results_data.append(['Shape', f"{results['shape_class']} ({results.get('shape_confidence', 0)*100:.1f}%)"])
-
- if results.get('maturity_class'):
- results_data.append(['Maturity', f"{results['maturity_class']} ({results.get('maturity_confidence', 0)*100:.1f}%)"])
-
- if results.get('ripeness_class'):
- results_data.append(['Ripeness', f"{results['ripeness_class']} ({results.get('ripeness_confidence', 0)*100:.1f}%)"])
-
- results_table = Table(results_data, colWidths=[2*inch, 3.5*inch])
- results_table.setStyle(TableStyle([
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3498db')),
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
- ('FONTSIZE', (0, 0), (-1, -1), 10),
- ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
- ('TOPPADDING', (0, 0), (-1, -1), 8),
- ('GRID', (0, 0), (-1, -1), 1, colors.grey),
- ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#ecf0f1')])
- ]))
- story.append(results_table)
- story.append(Spacer(1, 0.3*inch))
-
- # Overall Grade
- grade_color = {'A': '#27ae60', 'B': '#f39c12', 'C': '#e74c3c'}[report_content['grade']]
- grade_data = [
- [f"Overall Grade: Class {report_content['grade']}"],
- [report_content['grade_description']]
- ]
- grade_table = Table(grade_data, colWidths=[5.5*inch])
- grade_table.setStyle(TableStyle([
- ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor(grade_color)),
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
- ('FONTSIZE', (0, 0), (-1, 0), 14),
- ('FONTSIZE', (0, 1), (-1, 1), 10),
- ('BOTTOMPADDING', (0, 0), (-1, -1), 10),
- ('TOPPADDING', (0, 0), (-1, -1), 10),
- ('GRID', (0, 0), (-1, -1), 2, colors.HexColor(grade_color))
- ]))
- story.append(grade_table)
- story.append(Spacer(1, 0.3*inch))
-
- # Add visualizations if available
- if include_visualizations and visualizations:
- story.append(PageBreak())
- story.append(Paragraph("Visualizations", heading_style))
- story.append(Spacer(1, 0.2*inch))
-
- for viz_idx, (viz_title, viz_pixmap) in enumerate(visualizations):
- # Scale pixmap to PDF size
- max_width_points = 4.5*inch
- max_height_points = 4*inch
- pixmap = viz_pixmap
-
- # Scale if necessary, maintaining aspect ratio
- if pixmap.width() > max_width_points or pixmap.height() > max_height_points:
- aspect_ratio = pixmap.width() / pixmap.height()
- # Scale to fit within bounds
- new_height = int(max_height_points)
- new_width = int(new_height * aspect_ratio)
- if new_width > max_width_points:
- new_width = int(max_width_points)
- new_height = int(new_width / aspect_ratio)
- pixmap = pixmap.scaled(new_width, new_height, 1, 1) # Qt.KeepAspectRatio, Qt.SmoothTransformation
-
- # Save pixmap to temporary image file
- temp_img_path = os.path.join(tempfile.gettempdir(), f"report_viz_{int(time.time()*1000000)}_{viz_idx}.png")
- pixmap.save(temp_img_path, "PNG")
-
- try:
- # Add to PDF with calculated dimensions to fit properly
- story.append(Paragraph(viz_title, heading_style))
- # Use width with height=None to let ReportLab calculate proportional height
- img = RLImage(temp_img_path, width=4*inch, height=None)
- story.append(img)
- story.append(Spacer(1, 0.2*inch))
- except Exception as e:
- print(f"Error adding image to PDF: {e}")
-
- # Build PDF
- doc.build(story)
-
- # Auto-open the PDF file
- try:
- if os.name == 'nt': # Windows
- os.startfile(file_path)
- elif os.name == 'posix': # macOS/Linux
- import subprocess
- subprocess.Popen(['open', file_path])
- except Exception:
- pass
-
- QMessageBox.information(
- parent_widget,
- "PDF Exported Successfully",
- f"Report exported to:\n{file_path}"
- )
-
- return True
-
- except Exception as e:
- QMessageBox.critical(
- parent_widget,
- "PDF Export Error",
- f"Error creating PDF:\n{str(e)}"
- )
- return False
|