""" Export Engine — Generates downloadable files from extracted JSON field data. Supported formats: - CSV: Python csv module - XLSX: openpyxl with styled headers - DOCX: python-docx with a formatted field-value table - PDF: ReportLab with a clean, professional layout """ import csv import io from pathlib import Path from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from docx import Document from docx.shared import Inches, Pt, RGBColor from docx.enum.table import WD_TABLE_ALIGNMENT from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import mm from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer from models.schemas import ExtractedField def export_to_csv(fields: list[ExtractedField], filename: str = "export") -> bytes: """Generate a CSV file from extracted fields.""" output = io.StringIO() writer = csv.writer(output) # Header row writer.writerow(["Field Name", "Extracted Value"]) # Data rows for field in fields: writer.writerow([field.name, field.value]) return output.getvalue().encode("utf-8-sig") # BOM for Excel compatibility def export_to_xlsx(fields: list[ExtractedField], filename: str = "export") -> bytes: """Generate a styled XLSX file from extracted fields.""" wb = Workbook() ws = wb.active ws.title = "Extracted Data" # Styles header_font = Font(name="Calibri", bold=True, size=12, color="FFFFFF") header_fill = PatternFill(start_color="556B2F", end_color="556B2F", fill_type="solid") # Olive header_alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) cell_font = Font(name="Calibri", size=11) cell_alignment = Alignment(vertical="top", wrap_text=True) thin_border = Border( left=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin"), ) # Headers headers = ["Field Name", "Extracted Value"] for col, header in enumerate(headers, start=1): cell = ws.cell(row=1, column=col, value=header) cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border # Data rows for row_idx, field in enumerate(fields, start=2): # Openpyxl has a 32767 character limit per cell safe_val = str(field.value) if len(safe_val) > 32000: safe_val = safe_val[:32000] + "... [TRUNCATED]" ws.cell(row=row_idx, column=1, value=field.name).font = cell_font ws.cell(row=row_idx, column=2, value=safe_val).font = cell_font for col in range(1, 3): ws.cell(row=row_idx, column=col).alignment = cell_alignment ws.cell(row=row_idx, column=col).border = thin_border # Auto-width columns ws.column_dimensions["A"].width = 30 ws.column_dimensions["B"].width = 60 # Save to bytes output = io.BytesIO() wb.save(output) return output.getvalue() def export_to_docx(fields: list[ExtractedField], filename: str = "export") -> bytes: """Generate a formatted DOCX file from extracted fields.""" doc = Document() # Title title = doc.add_heading("Puff n Parse — Extracted Data", level=1) title_run = title.runs[0] title_run.font.color.rgb = RGBColor(0x55, 0x6B, 0x2F) # Olive doc.add_paragraph(f"Total fields extracted: {len(fields)}") doc.add_paragraph("") # Create a table with field data table = doc.add_table(rows=1, cols=2) table.alignment = WD_TABLE_ALIGNMENT.CENTER table.style = "Light Grid Accent 3" # Header row header_cells = table.rows[0].cells header_cells[0].text = "Field Name" header_cells[1].text = "Extracted Value" for cell in header_cells: for paragraph in cell.paragraphs: for run in paragraph.runs: run.font.bold = True run.font.size = Pt(11) # Data rows for field in fields: row_cells = table.add_row().cells row_cells[0].text = field.name safe_val = str(field.value) if len(safe_val) > 32000: safe_val = safe_val[:32000] + "... [TRUNCATED]" row_cells[1].text = safe_val # Footer doc.add_paragraph("") footer = doc.add_paragraph("Generated by Puff n Parse — From B 2 C") footer.runs[0].font.size = Pt(8) footer.runs[0].font.color.rgb = RGBColor(0x99, 0x99, 0x99) # Save to bytes output = io.BytesIO() doc.save(output) return output.getvalue() def export_to_pdf(fields: list[ExtractedField], filename: str = "export") -> bytes: """Generate a clean, professional PDF from extracted fields.""" output = io.BytesIO() doc = SimpleDocTemplate( output, pagesize=A4, topMargin=20 * mm, bottomMargin=20 * mm, leftMargin=15 * mm, rightMargin=15 * mm, ) styles = getSampleStyleSheet() # Custom styles title_style = ParagraphStyle( "CustomTitle", parent=styles["Title"], fontSize=18, textColor=colors.HexColor("#556B2F"), spaceAfter=12, ) normal_style = ParagraphStyle( "CustomNormal", parent=styles["Normal"], fontSize=10, leading=14, ) elements = [] # Title elements.append(Paragraph("Puff n Parse — Extracted Data", title_style)) elements.append(Paragraph(f"Total fields extracted: {len(fields)}", normal_style)) elements.append(Spacer(1, 12)) import xml.sax.saxutils as saxutils field_name_style = ParagraphStyle( "FieldName", parent=styles["Normal"], fontSize=11, fontName="Helvetica-Bold", textColor=colors.HexColor("#556B2F"), spaceAfter=4, ) for field in fields: safe_name = saxutils.escape(str(field.name)).replace('\n', '
') safe_val_str = str(field.value) if len(safe_val_str) > 32000: safe_val_str = safe_val_str[:32000] + "... [TRUNCATED]" safe_val = saxutils.escape(safe_val_str).replace('\n', '
') elements.append(Paragraph(safe_name, field_name_style)) elements.append(Paragraph(safe_val, normal_style)) elements.append(Spacer(1, 12)) elements.append(Spacer(1, 20)) # Footer footer_style = ParagraphStyle( "Footer", parent=styles["Normal"], fontSize=7, textColor=colors.HexColor("#999999"), ) elements.append(Paragraph("Generated by Puff n Parse — From B 2 C", footer_style)) doc.build(elements) return output.getvalue() def generate_export(fields: list[ExtractedField], format: str, filename: str = "export") -> tuple[bytes, str, str]: """ Generate an export file in the requested format. Returns: (file_bytes, content_type, file_extension) """ format_map = { "csv": (export_to_csv, "text/csv", ".csv"), "xlsx": (export_to_xlsx, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"), "docx": (export_to_docx, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx"), "pdf": (export_to_pdf, "application/pdf", ".pdf"), } if format not in format_map: raise ValueError(f"Unsupported export format: {format}") export_fn, content_type, extension = format_map[format] file_bytes = export_fn(fields, filename) return file_bytes, content_type, extension