Spaces:
Sleeping
Sleeping
| """ | |
| 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', '<br/>') | |
| 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', '<br/>') | |
| 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 | |