diff --git a/Dockerfile b/Dockerfile index 4af6e5b..04bed7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,13 @@ FROM docker.io/library/python:3.12-slim ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 +RUN apt-get update && apt-get install -y --no-install-recommends \ + libcairo2 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app COPY requirements.txt /app/requirements.txt RUN pip install --no-cache-dir -r /app/requirements.txt diff --git a/app.py b/app.py index 7e16cdb..9f85b2f 100644 --- a/app.py +++ b/app.py @@ -14,7 +14,7 @@ from datetime import datetime from pathlib import Path from typing import List, Optional -import fpdf as fpdflib +import weasyprint import markdown as md import requests from fastapi import FastAPI, File, Form, HTTPException, UploadFile @@ -261,67 +261,33 @@ def _split_thinking(text: str) -> tuple[str, str]: def _md_to_pdf(title: str, content_md: str) -> bytes: - """Render Markdown content to a PDF byte string.""" - - def _l1(text: str) -> str: - """Sanitize text to Latin-1; fpdf2 built-in fonts require it.""" - return text.encode("latin-1", errors="replace").decode("latin-1") - - _title = _l1(title[:100]) - - class PDF(fpdflib.FPDF): - def header(self): - self.set_font("Helvetica", "B", 9) - self.set_text_color(100, 100, 100) - self.cell(0, 6, _title, align="L") - self.ln(2) - self.set_draw_color(200, 200, 200) - self.line(self.l_margin, self.get_y(), self.w - self.r_margin, self.get_y()) - self.ln(4) - - def footer(self): - self.set_y(-12) - self.set_font("Helvetica", "I", 8) - self.set_text_color(160, 160, 160) - self.cell(0, 6, f"Seite {self.page_no()}", align="C") - - pdf = PDF(orientation="P", unit="mm", format="A4") - pdf.set_auto_page_break(auto=True, margin=15) - pdf.add_page() - - def _cell(text: str, size: int = 10, bold: bool = False, indent: float = 0, h: float = 5): - try: - pdf.set_font("Helvetica", "B" if bold else "", size) - pdf.set_text_color(20 if bold else 30, 20 if bold else 30, 20 if bold else 30) - if indent: - pdf.set_x(pdf.l_margin + indent) - pdf.multi_cell(pdf.w - pdf.l_margin - pdf.r_margin - indent, h, _l1(text)) - except Exception: - pass - - for raw_line in content_md.split("\n"): - line = raw_line.rstrip() - if line.startswith("# "): - _cell(line[2:], size=14, bold=True, h=7) - pdf.ln(1) - elif line.startswith("## "): - _cell(line[3:], size=12, bold=True, h=6) - pdf.ln(1) - elif line.startswith("### "): - _cell(line[4:], size=11, bold=True, h=6) - elif line.startswith(("- ", "* ", "+ ")): - _cell("• " + line[2:], indent=5) - elif re.match(r"^\d+\. ", line): - _cell(line, indent=5) - elif line in ("", "---"): - pdf.ln(3) - else: - clean = re.sub(r"\*\*(.+?)\*\*", r"\1", line) - clean = re.sub(r"\*(.+?)\*", r"\1", clean) - clean = re.sub(r"`(.+?)`", r"\1", clean) - _cell(clean) - - return bytes(pdf.output()) + """Render Markdown to PDF via weasyprint (HTML+CSS → PDF, full Unicode support).""" + body_html = md.markdown(content_md, extensions=["fenced_code", "tables", "nl2br"]) + html = f""" + + +{title} + + +

{title}

+{body_html} +""" + return weasyprint.HTML(string=html).write_pdf() def _collect_doc_chain(input_doc) -> list[dict]: diff --git a/requirements.txt b/requirements.txt index 2c1c626..959e558 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ uvicorn[standard]==0.32.1 requests==2.32.3 python-multipart==0.0.12 markdown==3.7 -fpdf2==2.8.3 +weasyprint>=62.3