Spaces:
Sleeping
Sleeping
File size: 3,614 Bytes
d27b187 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | """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)")
|