grantforge-api / backend /utils /export_documents.py
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
32.5 kB
import os
import markdown
import datetime
from bs4 import BeautifulSoup
def export_to_docx(
content: str,
output_path: str,
template: str = "standard",
project_title: str = "Wniosek o Dofinansowanie",
company_name: str = "Brak nazwy",
version: str = "1.0",
date_str: str = "",
extra_context: dict = None,
):
"""
Eksportuje wygenerowany przez Wizarda wniosek do formatu Microsoft Word (DOCX).
Wczytuje gotowy szablon docx z wymaganymi stalami i spisem treści.
Integruje się z `docxtpl` by umożliwić elastyczne wstrzykiwanie zmiennych (np {{ beneficjent.krs }}).
"""
if extra_context is None:
extra_context = {}
try:
from docxtpl import DocxTemplate
template_name = (
template if template in ["standard", "official", "modern"] else "standard"
)
template_path = os.path.join(
os.path.dirname(__file__),
"..",
"templates",
f"template_{template_name}.docx",
)
if not os.path.exists(template_path):
print(f"Brak pliku {template_path}, upewnij się, że wygenerowano szablony!")
return False
tpl = DocxTemplate(template_path)
# Puste wartości dla formatowania markdown (usuwamy duplikujące title jeśli zaczyna się od H1)
if content.startswith("# "):
content = "\n".join(content.split("\n")[1:])
# Przygotowanie pełnego kontekstu dla DocxTemplate (Jinja2 tags)
# Zostawiamy 'tresc_wniosku' puste, bo zastąpimy ten paragraf natywnym kodem python-docx
render_context = {
"tytul_projektu": project_title,
"nazwa_firmy": company_name,
"data_generowania": date_str,
"wersja": version,
"tresc_wniosku": "",
}
render_context.update(extra_context)
tpl.render(render_context)
# === Pełna integracja Świadectwa Zgodności w eksporcie (Cycle 4) ===
if extra_context and extra_context.get("include_grounding_certificate"):
append_grounding_certificate_to_docx(tpl._document, {
"title": project_title,
"company_name": company_name,
"version_hash": extra_context.get("version_hash"),
"precise_regulation_url": extra_context.get("precise_regulation_url"),
"regulation_link_quality": extra_context.get("regulation_link_quality"),
"snapshot_data": extra_context.get("snapshot_data"),
})
tpl.save(output_path)
# 2. Otwieramy zapisany plik za pomocą natywnego python-docx
import docx
doc = docx.Document(output_path)
# Usuwamy ostatni paragraf (który zawierał wyczyszczoną zmienną 'tresc_wniosku')
if len(doc.paragraphs) > 0 and doc.paragraphs[-1].text.strip() == "":
p = doc.paragraphs[-1]
p._element.getparent().remove(p._element)
# Konwersja MD do prostego HTML, a potem interpretacja BeautifulSoup
html = markdown.markdown(content)
soup = BeautifulSoup(html, "html.parser")
for element in soup:
if element.name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
level = int(element.name[1])
try:
doc.add_paragraph(element.text, style=f"Heading {level}")
except KeyError:
doc.add_heading(element.text, level=level)
elif element.name == "p":
# Złożona obsługa bold/italic
p = doc.add_paragraph()
if template == "official":
p.paragraph_format.alignment = 3 # Justify
for child in element.children:
if child.name is None:
p.add_run(child.string)
elif child.name in ["strong", "b"]:
p.add_run(child.text).bold = True
elif child.name in ["em", "i"]:
p.add_run(child.text).italic = True
else:
p.add_run(child.text)
elif element.name in ["ul", "ol"]:
for li in element.find_all("li"):
style_name = (
"List Bullet" if element.name == "ul" else "List Number"
)
try:
p = doc.add_paragraph(style=style_name)
except KeyError:
p = doc.add_paragraph(style="Normal")
p.add_run("• ")
for child in li.children:
if child.name is None:
p.add_run(child.string)
elif child.name in ["strong", "b"]:
p.add_run(child.text).bold = True
elif child.name in ["em", "i"]:
p.add_run(child.text).italic = True
else:
p.add_run(child.text)
elif element.name == "table":
# Ulepszona obsługa tabel dla DOCX
rows = element.find_all("tr")
if rows:
cols = max(len(row.find_all(["th", "td"])) for row in rows)
table = doc.add_table(rows=0, cols=cols)
try:
table.style = "Light Shading Accent 1"
except KeyError:
table.style = "Table Grid"
for idx_row, tr in enumerate(rows):
row = table.add_row()
cells = tr.find_all(["th", "td"])
for idx, cell in enumerate(cells):
if idx < cols:
p = row.cells[idx].paragraphs[0]
p.text = ""
for child in cell.children:
if child.name is None:
p.add_run(child.string.strip() if child.string else "")
elif child.name in ["strong", "b"]:
p.add_run(child.text.strip()).bold = True
elif child.name in ["em", "i"]:
p.add_run(child.text.strip()).italic = True
else:
p.add_run(child.text.strip())
# Zawsze pogrubiamy nagłówki (<th> lub pierwszy wiersz)
if cell.name == "th" or idx_row == 0:
for run in p.runs:
run.bold = True
doc.save(output_path)
return True
except Exception:
import traceback
print(f"Błąd eksportu do DOCX: {traceback.format_exc()}")
return False
def append_grounding_certificate_to_docx(doc, project_data: dict):
"""Dodaje stronę Świadectwa Zgodności na końcu dokumentu DOCX."""
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
doc.add_page_break()
# Nagłówek świadectwa
p = doc.add_paragraph()
run = p.add_run("ŚWIADECTWO ZGODNOŚCI WERSJI / REGULATION GROUNDING CERTIFICATE")
run.bold = True
run.font.size = Pt(14)
run.font.color.rgb = RGBColor(26, 54, 93)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p2 = doc.add_paragraph("GrantForge AI — Najwyższy poziom ugruntowania w regulaminach i prawie UE")
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.add_paragraph()
# Kluczowe dane
cert_info = [
("Projekt", project_data.get("title", "")),
("Beneficjent", project_data.get("company_name", "")),
("Data wygenerowania", datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")),
("Version Hash", project_data.get("version_hash", "N/A")),
("Precise Regulation URL", project_data.get("precise_regulation_url", "N/A")),
("Regulation Link Quality", project_data.get("regulation_link_quality", "N/A")),
]
for label, value in cert_info:
p = doc.add_paragraph()
run = p.add_run(f"{label}: ")
run.bold = True
p.add_run(str(value)[:120])
# Snapshot summary jeśli dostępny
if project_data.get("snapshot_data"):
doc.add_paragraph()
p = doc.add_paragraph()
p.add_run("SNAPSHOT REGULAMINU (źródło prawdy)").bold = True
for k, v in project_data["snapshot_data"].items():
if k in ["key_rules", "exclusions"]:
continue
doc.add_paragraph(f" {k}: {v}")
doc.add_paragraph()
p = doc.add_paragraph("Dokument ten potwierdza, że wszystkie wygenerowane treści opierają się na konkretnej, wersjonowanej wersji regulaminu oraz aktualnych aktach prawnych UE.")
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
p = doc.add_paragraph("https://grantforge.ai | Regulation Engine jako źródło prawdy")
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
def export_full_application_with_certificate(
output_path: str,
project_title: str,
company_name: str,
main_content: str,
certificate_data: dict,
) -> bool:
"""Eksportuje pełny wniosek + stronę Świadectwa Zgodności na końcu (PDF). Używane w cyklu finałowym."""
try:
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from reportlab.lib.styles import getSampleStyleSheet
doc = SimpleDocTemplate(output_path, pagesize=A4)
styles = getSampleStyleSheet()
story = []
# Główna treść wniosku (uproszczona)
for line in (main_content or "").split("\n")[:80]:
if line.strip():
story.append(Paragraph(line[:200], styles["Normal"]))
story.append(PageBreak())
# Świadectwo
cert = certificate_data or {}
story.append(Paragraph("ŚWIADECTWO ZGODNOŚCI WERSJI / REGULATION GROUNDING CERTIFICATE", styles["Heading1"]))
story.append(Spacer(1, 10))
story.append(Paragraph(f"Projekt: {project_title}", styles["Normal"]))
story.append(Paragraph(f"Beneficjent: {company_name}", styles["Normal"]))
story.append(Paragraph(f"Version Hash: {cert.get('version_hash', 'N/A')}", styles["Normal"]))
story.append(Paragraph(f"Precise Regulation URL: {cert.get('precise_regulation_url', 'N/A')}", styles["Normal"]))
story.append(Paragraph(f"Link Quality: {cert.get('regulation_link_quality', 'N/A')}", styles["Normal"]))
doc.build(story)
return True
except Exception as e:
print(f"Błąd eksportu PDF z certificate: {e}")
return False
def export_grounding_certificate_pdf(
output_path: str,
project_title: str,
company_name: str,
snapshot_data: dict,
eurlex_links: list = None,
pkd_list: list = None,
msp_status: str = "",
engine_checks: dict = None,
version_hash: str = "",
v5_certificate: dict = None, # v5.0 extended: citation_score, trap_risk, trust_score etc.
) -> bool:
"""
Generuje publiczne, profesjonalne Świadectwo Zgodności (Regulation Grounding Certificate) w formacie PDF.
Zawiera wszystkie metadane snapshotu, hash, linki EUR-Lex, PKD, status MSP, wyniki silnika.
To jest kluczowy element dla najwyższej wiarygodności — dokument można załączyć do wniosku lub pokazać instytucji.
"""
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
import datetime
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
rightMargin=1.5*cm,
leftMargin=1.5*cm,
topMargin=1.5*cm,
bottomMargin=1.5*cm
)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
'TitlePL',
parent=styles['Heading1'],
fontSize=16,
alignment=TA_CENTER,
spaceAfter=12,
textColor=colors.HexColor('#1a365d')
)
subtitle_style = ParagraphStyle(
'SubtitlePL',
parent=styles['Normal'],
fontSize=10,
alignment=TA_CENTER,
textColor=colors.HexColor('#2d3748')
)
body_style = ParagraphStyle(
'BodyPL',
parent=styles['Normal'],
fontSize=9,
alignment=TA_JUSTIFY,
spaceAfter=6
)
small_style = ParagraphStyle(
'SmallPL',
parent=styles['Normal'],
fontSize=8,
textColor=colors.gray
)
story = []
# Header
story.append(Paragraph("ŚWIADECTWO ZGODNOŚCI WERSJI / REGULATION GROUNDING CERTIFICATE", title_style))
story.append(Paragraph("GrantForge AI — Najwyższy poziom ugruntowania w aktualnych regulaminach i prawie UE", subtitle_style))
story.append(Spacer(1, 0.4*cm))
story.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor('#1a365d')))
story.append(Spacer(1, 0.3*cm))
# Basic info
now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
info_data = [
["Projekt:", project_title],
["Beneficjent:", company_name],
["Data wygenerowania świadectwa:", now],
["Wersja silnika:", "Regulation Engine v3 + Live EUR-Lex"],
]
info_table = Table(info_data, colWidths=[5*cm, 11*cm])
info_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 9),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
]))
story.append(info_table)
story.append(Spacer(1, 0.3*cm))
# Snapshot section
story.append(Paragraph("<b>ŹRÓDŁO PRAWNE (SNAPSHOT REGULAMINU)</b>", body_style))
if snapshot_data:
snap_text = f"""
<b>Program:</b> {snapshot_data.get('program', 'N/A')}<br/>
<b>Snapshot ID:</b> {snapshot_data.get('id', 'N/A')}<br/>
<b>Version Hash (SHA256 skrót):</b> {snapshot_data.get('version_hash') or version_hash or 'N/A'}<br/>
<b>Data pobrania:</b> {snapshot_data.get('fetched_at', 'N/A')}<br/>
<b>Źródło:</b> {snapshot_data.get('source_url', 'N/A')}<br/>
<b>Wersja dokumentu:</b> {snapshot_data.get('document_version', 'N/A')}<br/>
<b>Obowiązuje od:</b> {snapshot_data.get('effective_date', 'N/A')}<br/>
<b>Instytucja:</b> {snapshot_data.get('source_institution', 'N/A')}<br/>
<b>Liczba kluczowych reguł:</b> {len(snapshot_data.get('key_rules', []))}<br/>
<b>Liczba wykluczeń:</b> {len(snapshot_data.get('exclusions', []))}
"""
story.append(Paragraph(snap_text, body_style))
else:
story.append(Paragraph("Brak snapshotu — dokument wygenerowany bez pełnego ugruntowania.", body_style))
story.append(Spacer(1, 0.3*cm))
# v5.0 Extended Grounding Certificate fields (citation/trap/trust)
if v5_certificate:
story.append(Paragraph("<b>v5.0 MASTER ORCHESTRATOR GROUNDING CERTIFICATE (EXPORTABLE)</b>", body_style))
v5_text = f"""
<b>Snapshot ID:</b> {v5_certificate.get('snapshot_id', 'N/A')}<br/>
<b>Version Hash:</b> {v5_certificate.get('version_hash', version_hash or 'N/A')}<br/>
<b>Effective Date:</b> {v5_certificate.get('effective_date', 'N/A')}<br/>
<b>Citation Score:</b> {v5_certificate.get('citation_score', 0.0)}<br/>
<b>Trap Risk (Kruczkowski):</b> {v5_certificate.get('trap_risk', 'unknown')}<br/>
<b>Trust Score:</b> {v5_certificate.get('trust_score', 50)}/100<br/>
<b>Data Quality Avg:</b> {v5_certificate.get('data_quality_avg', 0.0)}<br/>
<b>Timestamp:</b> {v5_certificate.get('timestamp', '')}<br/>
<b>Query Intent:</b> {v5_certificate.get('query_intent', '')}<br/>
<b>Synthesis:</b> {str(v5_certificate.get('synthesis_summary', ''))[:180]}
"""
story.append(Paragraph(v5_text, body_style))
story.append(Spacer(1, 0.2*cm))
# EUR-Lex
story.append(Paragraph("<b>LIVE LEGAL CONTROL — EUR-LEX (ŹRÓDŁO PRAWY UE)</b>", body_style))
if eurlex_links:
for link in eurlex_links[:3]:
story.append(Paragraph(f"• {link.get('title', '')}{link.get('url', '')}", small_style))
else:
story.append(Paragraph("Brak bezpośrednich linków EUR-Lex w tym świadectwie.", small_style))
story.append(Spacer(1, 0.3*cm))
# PKD + MSP
story.append(Paragraph("<b>UZIEMIENIE W DANYCH FIRMY (GUS + MSP)</b>", body_style))
if pkd_list:
story.append(Paragraph(f"Kody PKD wnioskodawcy (GUS): {', '.join(pkd_list[:5])}", body_style))
story.append(Paragraph(f"Status MŚP: {msp_status or 'Zweryfikowany przez GraphRAG'}", body_style))
story.append(Spacer(1, 0.3*cm))
# Engine checks
if engine_checks:
story.append(Paragraph("<b>WYNIKI AKTYWNEJ WERYFIKACJI (REGULATION ENGINE)</b>", body_style))
story.append(Paragraph(str(engine_checks), body_style))
story.append(Spacer(1, 0.5*cm))
story.append(HRFlowable(width="100%", thickness=1, color=colors.gray))
story.append(Paragraph(
"To świadectwo potwierdza, że wszystkie wygenerowane treści opierają się na konkretnej, wersjonowanej wersji regulaminu + aktualnych aktach prawnych UE (EUR-Lex). "
"Hash wersji pozwala na późniejszą weryfikację. Dokument jest integralną częścią wniosku i może być przedstawiony instytucji finansującej.",
small_style
))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph("GrantForge AI — Regulation Engine jako źródło prawdy | https://grantforge.ai", subtitle_style))
doc.build(story)
return True
except ImportError:
print("Brak reportlab — zainstaluj: pip install reportlab")
# Fallback: zapisz jako tekst
with open(output_path.replace('.pdf', '.txt'), 'w', encoding='utf-8') as f:
f.write(f"ŚWIADECTWO ZGODNOŚCI — {project_title}\nHash: {version_hash}\nSnapshot: {snapshot_data}\n")
return False
except Exception as e:
print(f"Błąd generowania Świadectwa PDF: {e}")
return False
def get_pdf_css(template: str) -> str:
if template == "official":
return """
@page { size: A4; margin: 2.5cm; }
body { font-family: "DejaVu Sans", "Arial", serif; font-size: 11pt; line-height: 1.5; text-align: justify; color: #000; }
h1, h2, h3 { color: #000; page-break-after: avoid; font-family: "DejaVu Sans", "Arial", serif; }
h1 { border-bottom: 2px solid #000; padding-bottom: 5px; text-transform: uppercase; text-align: center; font-size: 16pt; margin-top: 2em; }
h2 { font-size: 14pt; margin-top: 1.5em; }
h3 { font-size: 12pt; margin-top: 1.2em; font-style: italic; }
table { width: 100%; border-collapse: collapse; margin: 1em 0; page-break-inside: avoid; }
th, td { border: 1px solid #000; padding: 8px; text-align: left; }
p { margin-bottom: 1em; orphans: 3; widows: 3; }
a { color: #000; text-decoration: none; }
.toc { page-break-after: always; }
.toc ul { list-style-type: none; padding-left: 1.5em; }
.toc > ul { padding-left: 0; }
.toc a { text-decoration: none; color: #000; }
"""
elif template == "modern":
return """
@page { size: A4; margin: 2.5cm; }
body { font-family: "DejaVu Sans", "Arial", sans-serif; font-size: 11pt; line-height: 1.6; color: #1e293b; background: #fff; }
h1, h2, h3 { page-break-after: avoid; color: #0f172a; font-family: "DejaVu Sans", "Arial", sans-serif; }
h1 { border-bottom: 2px solid #3b82f6; padding-bottom: 0.5em; font-size: 24pt; margin-top: 1em; }
h2 { border-bottom: 1px solid #e2e8f0; padding-bottom: 0.3em; font-size: 18pt; margin-top: 1.5em; color: #2563eb; }
h3 { font-size: 14pt; margin-top: 1.2em; color: #334155; }
p { margin-bottom: 1em; text-align: justify; orphans: 3; widows: 3; }
table { width: 100%; border-collapse: collapse; margin: 1.5em 0; background: #f8fafc; font-size: 10pt; page-break-inside: avoid; }
th, td { border: 1px solid #e2e8f0; padding: 10px 14px; text-align: left; }
th { background-color: #e2e8f0; color: #1e293b; font-weight: bold; border-bottom: 2px solid #cbd5e1; }
tr:nth-child(even) { background-color: #f1f5f9; }
ul, ol { margin-bottom: 1em; padding-left: 2em; }
li { margin-bottom: 0.5em; }
.toc { page-break-after: always; padding: 2em; background: #f8fafc; border-radius: 8px; }
.toc ul { list-style-type: none; padding-left: 1.5em; }
.toc > ul { padding-left: 0; }
.toc a { text-decoration: none; color: #4f46e5; border-bottom: 1px dotted #cbd5e1; display: block; padding-bottom: 5px; margin-bottom: 5px; }
"""
elif template == "enterprise":
return """
@page { size: A4; margin: 2.5cm; }
body { font-family: "DejaVu Sans", "Arial", sans-serif; font-size: 11pt; line-height: 1.6; color: #1f2937; background: #fff; }
h1, h2, h3 { page-break-after: avoid; color: #1e3a8a; font-family: "DejaVu Sans", "Arial", sans-serif; }
h1 { border-bottom: 2px solid #10b981; padding-bottom: 0.5em; font-size: 24pt; margin-top: 1em; }
h2 { border-bottom: 1px solid #e5e7eb; padding-bottom: 0.3em; font-size: 18pt; margin-top: 1.5em; color: #1e40af; }
h3 { font-size: 14pt; margin-top: 1.2em; color: #374151; }
p { margin-bottom: 1em; text-align: justify; orphans: 3; widows: 3; }
table { width: 100%; border-collapse: collapse; margin: 1.5em 0; background: #ffffff; }
th, td { border: 1px solid #d1d5db; padding: 12px; text-align: left; }
th { background-color: #f3f4f6; color: #1f2937; font-weight: bold; }
ul, ol { margin-bottom: 1em; padding-left: 2em; }
li { margin-bottom: 0.5em; }
.toc { page-break-after: always; padding: 2em; background: #f9fafb; border-radius: 8px; border-left: 4px solid #10b981; }
.toc ul { list-style-type: none; padding-left: 1.5em; }
.toc > ul { padding-left: 0; }
.toc a { text-decoration: none; color: #1e3a8a; border-bottom: 1px dotted #9ca3af; display: block; padding-bottom: 5px; margin-bottom: 5px; }
"""
else: # standard
return """
@page { size: A4; margin: 2cm; }
body { font-family: "DejaVu Sans", "Arial", sans-serif; font-size: 11pt; line-height: 1.6; color: #333; }
h1, h2, h3 { page-break-after: avoid; }
h1 { border-bottom: 2px solid #3498db; padding-bottom: 10px; color: #2c3e50; }
h2 { color: #2980b9; margin-top: 1.5em; }
table { width: 100%; border-collapse: collapse; margin: 1em 0; }
th, td { border: 1px solid #bdc3c7; padding: 8px; text-align: left; }
th { background-color: #ecf0f1; }
p { margin-bottom: 1em; text-align: justify; orphans: 3; widows: 3; }
.toc { page-break-after: always; margin-top: 2em; }
.toc ul { list-style-type: none; padding-left: 1.5em; }
.toc > ul { padding-left: 0; }
.toc a { text-decoration: none; color: #2980b9; border-bottom: 1px dotted #bdc3c7; display: block; padding-bottom: 5px; margin-bottom: 5px; }
"""
def export_to_pdf(
content: str,
output_path: str,
template: str = "standard",
project_title: str = "Wniosek o Dofinansowanie",
company_name: str = "Brak nazwy",
version: str = "1.0",
date_str: str = "",
extra_context: dict = None,
):
"""
Eksportuje wniosek do PDF wykorzystując WeasyPrint.
"""
if extra_context is None:
extra_context = {}
try:
from xhtml2pdf import pisa
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase import pdfmetrics
import xhtml2pdf.default
except ImportError:
print("Nie mozna pobrac xhtml2pdf.")
raise Exception("Należy zainstalować xhtml2pdf (pip install xhtml2pdf).")
try:
# Usuwamy ewentualny nadmiarowy title na poczatku markdown by nie dublowac cover page
if content.startswith("# "):
content = "\n".join(content.split("\n")[1:])
md_content = f"[TOC]\n\n{content}"
html_body = markdown.markdown(
md_content,
extensions=["tables", "fenced_code", "toc"],
extension_configs={'toc': {'title': 'Spis Treści'}}
)
import urllib.request
backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
font_dir = os.path.join(backend_dir, "assets")
os.makedirs(font_dir, exist_ok=True)
dejavu_path = os.path.join(backend_dir, "DejaVuSans.ttf")
dejavu_bold_path = os.path.join(backend_dir, "DejaVuSans-Bold.ttf")
if not os.path.exists(dejavu_path):
try:
urllib.request.urlretrieve(
"https://cdn.jsdelivr.net/npm/@vintproykt/dejavu-fonts-ttf/ttf/DejaVuSans.ttf",
dejavu_path,
)
except Exception as e:
print(f"Nie udało się pobrać DejaVuSans: {e}")
if not os.path.exists(dejavu_bold_path):
try:
urllib.request.urlretrieve(
"https://cdn.jsdelivr.net/npm/@vintproykt/dejavu-fonts-ttf/ttf/DejaVuSans-Bold.ttf",
dejavu_bold_path,
)
except Exception as e:
print(f"Nie udało się pobrać DejaVuSans-Bold: {e}")
font_face = ""
if os.path.exists(dejavu_path):
pdfmetrics.registerFont(TTFont("DejaVu Sans", dejavu_path))
if os.path.exists(dejavu_bold_path):
pdfmetrics.registerFont(TTFont("DejaVu Sans-Bold", dejavu_bold_path))
xhtml2pdf.default.DEFAULT_FONT["helvetica"] = "DejaVu Sans"
xhtml2pdf.default.DEFAULT_FONT["sans-serif"] = "DejaVu Sans"
xhtml2pdf.default.DEFAULT_FONT["arial"] = "DejaVu Sans"
font_face = f"""
@font-face {{ font-family: "DejaVu Sans"; src: url("file://{dejavu_path}"); }}
"""
if os.path.exists(dejavu_bold_path):
font_face += f"""
@font-face {{ font-family: "DejaVu Sans"; font-weight: bold; src: url("file://{dejavu_bold_path}"); }}
"""
css_style = font_face + get_pdf_css(template)
if not date_str:
date_str = datetime.datetime.now().strftime("%d.%m.%Y")
logo_html = ""
if template == "enterprise":
logo_html = '<div style="color: #10b981; font-size: 24pt; font-weight: bold; margin-bottom: 20px;">♦ GrantForge</div>'
# Cert logic
cert_html = ""
if extra_context and extra_context.get("include_grounding_certificate"):
cert = extra_context
snap = cert.get("snapshot_data", {})
cert_html = f"""
<pdf:nextpage />
<div style="margin-top: 50px;">
<h1 style="color: #1a365d; text-align: center; border-bottom: none;">ŚWIADECTWO ZGODNOŚCI WERSJI / REGULATION GROUNDING CERTIFICATE</h1>
<p style="text-align: center; color: #2d3748;">GrantForge AI — Najwyższy poziom ugruntowania w regulaminach i prawie UE</p>
<hr style="border: 1px solid #1a365d; margin-top: 20px; margin-bottom: 20px;" />
<p><b>Projekt:</b> {project_title}</p>
<p><b>Beneficjent:</b> {company_name}</p>
<p><b>Data wygenerowania:</b> {datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")}</p>
<p><b>Version Hash:</b> {cert.get("version_hash", "N/A")}</p>
<p><b>Precise Regulation URL:</b> {cert.get("precise_regulation_url", "N/A")}</p>
<p><b>Link Quality:</b> {cert.get("regulation_link_quality", "N/A")}</p>
"""
if snap:
cert_html += "<h3>SNAPSHOT REGULAMINU (źródło prawdy)</h3><ul>"
for k, v in snap.items():
if k not in ["key_rules", "exclusions"]:
cert_html += f"<li><b>{k}:</b> {v}</li>"
cert_html += "</ul>"
cert_html += """
<hr style="border: 1px solid #cbd5e1; margin-top: 30px; margin-bottom: 20px;" />
<p style="text-align: justify; font-size: 9pt; color: #64748b;">
To świadectwo potwierdza, że wszystkie wygenerowane treści opierają się na konkretnej, wersjonowanej wersji regulaminu oraz aktualnych aktach prawnych UE.
</p>
<p style="text-align: center; font-size: 10pt; color: #475569;">
https://grantforge.ai | Regulation Engine jako źródło prawdy
</p>
</div>
"""
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
{css_style}
.cover-page {{
text-align: center;
padding-top: 250px;
}}
</style>
</head>
<body>
<div class="cover-page">
{logo_html}
<h1 style="border: none; font-size: 32pt; margin-bottom: 20px;">{project_title}</h1>
<p style="font-size: 16pt; color: #2c3e50; font-weight: bold;">{company_name}</p>
<div style="margin-top: 150px;">
<p style="font-size: 11pt; color: #95a5a6; font-weight: bold; margin-bottom: 5px;">Wygenerowano z użyciem systemu wsparcia DotacjeAI</p>
<p style="font-size: 10pt; color: #95a5a6;">Dokument utworzony: {date_str} | Wersja: {version}</p>
</div>
</div>
<!-- Wymuszenie nowej strony po stronie tytułowej -->
<pdf:nextpage />
{html_body}
{cert_html}
</body>
</html>
"""
with open(output_path, "wb") as pdf_file:
pisa_status = pisa.CreatePDF(html_content.encode("utf-8"), dest=pdf_file, encoding='utf-8')
if pisa_status.err:
print(f"Błąd pisa: {pisa_status.err}")
return False
return True
except Exception:
import traceback
print(f"Błąd eksportu do PDF: {traceback.format_exc()}")
return False
def localize_markdown_headings(content: str, lang: str = "pl") -> str:
"""Map English section headings to Polish (lang=pl)."""
if lang != "pl" or not content:
return content
mapping = {
"Executive Summary": "Streszczenie wykonawcze",
"Project Description": "Opis projektu",
"Budget": "Budżet",
"Timeline": "Harmonogram",
"Risk Analysis": "Analiza ryzyka",
"Expected Results": "Oczekiwane rezultaty",
}
lines = content.splitlines()
out: list[str] = []
for line in lines:
new_line = line
for en, pl in mapping.items():
if line.strip() == f"# {en}":
new_line = f"# {pl}"
break
if line.strip() == f"## {en}":
new_line = f"## {pl}"
break
if line.strip() == en:
new_line = pl
break
out.append(new_line)
return "\n".join(out)