145 lines
3.7 KiB
Python
145 lines
3.7 KiB
Python
"""Generate PDF: Scanned letter FROM client (simulated scan effect)."""
|
|
|
|
import io
|
|
from pathlib import Path
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.units import mm
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.lib.colors import HexColor
|
|
|
|
try:
|
|
from PIL import Image, ImageFilter, ImageEnhance
|
|
import random as _random
|
|
|
|
HAS_PIL = True
|
|
except ImportError:
|
|
HAS_PIL = False
|
|
|
|
|
|
def _create_clean_pdf(
|
|
buffer, from_name, from_company, from_address, to_name, to_firm, body, date
|
|
):
|
|
"""Create a clean PDF letter."""
|
|
c = canvas.Canvas(buffer, pagesize=A4)
|
|
w, h = A4
|
|
|
|
# Company header
|
|
c.setFont("Helvetica-Bold", 14)
|
|
c.drawString(50, h - 60, from_company)
|
|
c.setFont("Helvetica", 9)
|
|
y = h - 78
|
|
for line in from_address.split(","):
|
|
c.drawString(50, y, line.strip())
|
|
y -= 14
|
|
|
|
# Date
|
|
y -= 20
|
|
c.setFont("Helvetica", 11)
|
|
c.drawString(50, y, date)
|
|
|
|
# Recipient
|
|
y -= 30
|
|
c.drawString(50, y, f"Mr. {to_name}")
|
|
y -= 16
|
|
c.drawString(50, y, to_firm)
|
|
y -= 16
|
|
c.drawString(50, y, "Suites 3203-3207, 32/F, Edinburgh Tower, The Landmark")
|
|
y -= 16
|
|
c.drawString(50, y, "15 Queen's Road Central, Central, Hong Kong")
|
|
|
|
# Body
|
|
y -= 35
|
|
c.setFont("Helvetica", 11)
|
|
for para in body.split("\n\n"):
|
|
for line_text in _wrap_text(para.strip(), 85):
|
|
if y < 80:
|
|
c.showPage()
|
|
y = h - 60
|
|
c.setFont("Helvetica", 11)
|
|
c.drawString(50, y, line_text)
|
|
y -= 16
|
|
y -= 10
|
|
|
|
# Signature area
|
|
y -= 20
|
|
c.drawString(50, y, "Yours sincerely,")
|
|
y -= 40
|
|
# Simulate a signature squiggle
|
|
c.setStrokeColor(HexColor("#1a1a8a"))
|
|
c.setLineWidth(1.5)
|
|
import random
|
|
|
|
random.seed(hash(from_name))
|
|
sx = 50
|
|
sy = y + 10
|
|
c.line(sx, sy, sx + 30, sy + 8)
|
|
c.line(sx + 30, sy + 8, sx + 50, sy - 5)
|
|
c.line(sx + 50, sy - 5, sx + 80, sy + 3)
|
|
c.setStrokeColor(HexColor("#000000"))
|
|
|
|
y -= 10
|
|
c.setFont("Helvetica-Bold", 11)
|
|
c.drawString(50, y, from_name)
|
|
y -= 16
|
|
c.setFont("Helvetica", 10)
|
|
c.drawString(50, y, from_company)
|
|
|
|
c.save()
|
|
|
|
|
|
def _wrap_text(text, max_chars):
|
|
"""Simple word-wrap."""
|
|
words = text.split()
|
|
lines = []
|
|
current = ""
|
|
for word in words:
|
|
if len(current) + len(word) + 1 > max_chars:
|
|
lines.append(current)
|
|
current = word
|
|
else:
|
|
current = f"{current} {word}" if current else word
|
|
if current:
|
|
lines.append(current)
|
|
return lines
|
|
|
|
|
|
def generate(
|
|
output_path: Path,
|
|
from_name: str,
|
|
from_company: str,
|
|
from_address: str,
|
|
to_name: str,
|
|
to_firm: str,
|
|
body: str,
|
|
date: str,
|
|
):
|
|
"""Generate a scanned-looking PDF letter."""
|
|
# First create a clean PDF in memory
|
|
buf = io.BytesIO()
|
|
_create_clean_pdf(
|
|
buf, from_name, from_company, from_address, to_name, to_firm, body, date
|
|
)
|
|
buf.seek(0)
|
|
|
|
if HAS_PIL:
|
|
# Convert to image, degrade slightly, save back as PDF
|
|
try:
|
|
from pdf2image import convert_from_bytes
|
|
|
|
images = convert_from_bytes(buf.read(), dpi=150)
|
|
if images:
|
|
img = images[0]
|
|
# Add slight grey tint (paper simulation)
|
|
enhancer = ImageEnhance.Brightness(img)
|
|
img = enhancer.enhance(0.95)
|
|
# Slight blur for scan effect
|
|
img = img.filter(ImageFilter.GaussianBlur(radius=0.3))
|
|
# Save as PDF
|
|
img.save(str(output_path), "PDF", resolution=150)
|
|
return
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback: save clean PDF as-is
|
|
output_path.write_bytes(buf.getvalue())
|