From 2f031591bfab3f4c3331395efd0ce5c675435463 Mon Sep 17 00:00:00 2001 From: RGalyaviev Date: Thu, 4 Sep 2025 08:48:36 +0300 Subject: [PATCH] Export: add /p/.pdf and /p/.docx (xhtml2pdf + htmldocx); overlay export buttons for all users; minor HTML wrapping --- app.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++-- requirements.txt | 3 +++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index bf8e73e..351ee2f 100644 --- a/app.py +++ b/app.py @@ -15,6 +15,10 @@ from flask import ( flash, Response, ) +from io import BytesIO +from xhtml2pdf import pisa # type: ignore +from docx import Document # type: ignore +from htmldocx import HtmlToDocx # type: ignore def create_app(): @@ -238,14 +242,64 @@ def create_app(): "Made by Ruslan" "" ) + # Export buttons (visible to everyone). If admin overlay exists, offset a bit. + top_offset = "60px" if is_logged_in() else "16px" + pdf_url = url_for("export_pdf", uid=uid) + docx_url = url_for("export_docx", uid=uid) + exports = ( + f'
' + f'PDF' + f'Word' + '
' + ) + overlays = wm + exports lower_all = html.lower() if "" in lower_all: i2 = lower_all.rfind("") - html = html[:i2] + wm + html[i2:] + html = html[:i2] + overlays + html[i2:] else: - html = html + wm + html = html + overlays return Response(html, mimetype="text/html; charset=utf-8") + def _fetch_page(uid: str): + db = get_db() + row = db.execute("SELECT title, html FROM pages WHERE uuid = ?", (uid,)).fetchone() + if row is None: + abort(404) + return row + + def _wrap_html_for_export(title: str, html: str) -> str: + head_title = f"{title}" if title else "" + return ( + "" + head_title + + "" + + html + "" + ) + + @app.route("/p/.pdf") + def export_pdf(uid: str): + row = _fetch_page(uid) + title = row["title"] or f"page-{uid[:8]}" + html_doc = _wrap_html_for_export(title, row["html"]) + out = BytesIO() + pisa.CreatePDF(src=html_doc, dest=out) + out.seek(0) + headers = {"Content-Disposition": f"attachment; filename={title}.pdf"} + return Response(out.read(), mimetype="application/pdf", headers=headers) + + @app.route("/p/.docx") + def export_docx(uid: str): + row = _fetch_page(uid) + title = row["title"] or f"page-{uid[:8]}" + html_doc = _wrap_html_for_export(title, row["html"]) + doc = Document() + HtmlToDocx().add_html_to_document(html_doc, doc) + out = BytesIO() + doc.save(out) + out.seek(0) + headers = {"Content-Disposition": f"attachment; filename={title}.docx"} + return Response(out.read(), mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", headers=headers) + # Optional: simple 404 page to keep things minimal @app.errorhandler(404) def not_found(_): diff --git a/requirements.txt b/requirements.txt index e3c27dc..787e7b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ Flask>=2.3,<3.0 gunicorn>=21.2 +python-docx>=1.1 +htmldocx>=0.0.6 +xhtml2pdf>=0.2.15