"""
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