Spaces:
Sleeping
Sleeping
| """Shared Word/PDF renderers for AgroSense documents. | |
| A document is (title, subtitle, sections). Each section is (heading, [blocks]); | |
| a block is (kind, content) where kind is: | |
| "h2" -> subheading str ; "p" -> paragraph str ; "ul"/"ol" -> list[str]. | |
| Keep content ASCII so the PDF renders with built-in core fonts (no bundled TTF). | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import List, Tuple | |
| Section = Tuple[str, list] | |
| def _compose_docx(title: str, subtitle: str, sections: List[Section]): | |
| from docx import Document | |
| doc = Document() | |
| doc.add_heading(title, 0) | |
| if subtitle: | |
| doc.add_paragraph(subtitle) | |
| for heading, blocks in sections: | |
| doc.add_heading(heading, level=1) | |
| for kind, content in blocks: | |
| if kind == "h2": | |
| doc.add_heading(content, level=2) | |
| elif kind == "p": | |
| doc.add_paragraph(content) | |
| elif kind == "ul": | |
| for item in content: | |
| doc.add_paragraph(item, style="List Bullet") | |
| elif kind == "ol": | |
| for item in content: | |
| doc.add_paragraph(item, style="List Number") | |
| return doc | |
| def _compose_pdf(title: str, subtitle: str, sections: List[Section]): | |
| from fpdf import FPDF | |
| from fpdf.enums import XPos, YPos | |
| pdf = FPDF() | |
| pdf.set_auto_page_break(True, margin=16) | |
| pdf.add_page() | |
| def cell(s, size=11, style="", lh=5.5, indent=0): | |
| pdf.set_font("Helvetica", style, size) | |
| if indent: | |
| pdf.set_x(pdf.l_margin + indent) | |
| pdf.multi_cell(0, lh, s, new_x=XPos.LMARGIN, new_y=YPos.NEXT) | |
| cell(title, size=18, style="B", lh=9) | |
| if subtitle: | |
| cell(subtitle, size=11, lh=6) | |
| pdf.ln(3) | |
| for heading, blocks in sections: | |
| pdf.ln(2) | |
| cell(heading, size=14, style="B", lh=7) | |
| for kind, content in blocks: | |
| if kind == "h2": | |
| pdf.ln(1) | |
| cell(content, size=12, style="B", lh=6) | |
| elif kind == "p": | |
| cell(content) | |
| elif kind in ("ul", "ol"): | |
| for i, item in enumerate(content, 1): | |
| prefix = f"{i}. " if kind == "ol" else "- " | |
| cell(prefix + item, indent=5) | |
| pdf.ln(1) | |
| return pdf | |
| def build_docx(title: str, subtitle: str, sections: List[Section], path: Path) -> None: | |
| _compose_docx(title, subtitle, sections).save(str(path)) | |
| def build_pdf(title: str, subtitle: str, sections: List[Section], path: Path) -> None: | |
| _compose_pdf(title, subtitle, sections).output(str(path)) | |
| def build_docx_bytes(title: str, subtitle: str, sections: List[Section]) -> bytes: | |
| """Render a .docx to bytes (for streaming a generated document).""" | |
| import io | |
| buf = io.BytesIO() | |
| _compose_docx(title, subtitle, sections).save(buf) | |
| return buf.getvalue() | |
| def build_pdf_bytes(title: str, subtitle: str, sections: List[Section]) -> bytes: | |
| """Render a .pdf to bytes (for streaming a generated document).""" | |
| return bytes(_compose_pdf(title, subtitle, sections).output()) | |
| def build_both(title: str, subtitle: str, sections: List[Section], | |
| out_dir: Path, basename: str) -> None: | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| docx_path = out_dir / f"{basename}.docx" | |
| pdf_path = out_dir / f"{basename}.pdf" | |
| build_docx(title, subtitle, sections, docx_path) | |
| build_pdf(title, subtitle, sections, pdf_path) | |
| print(f"Wrote {docx_path} ({docx_path.stat().st_size} b) and " | |
| f"{pdf_path} ({pdf_path.stat().st_size} b)") | |