First commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# JingTian sample document generators
|
||||
@@ -0,0 +1,60 @@
|
||||
"""CJK font registration helper for reportlab using WenQuanYi Micro Hei TTF."""
|
||||
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
_REGISTERED = False
|
||||
CJK_FONT = "Helvetica" # fallback
|
||||
|
||||
|
||||
def register_cjk_font():
|
||||
"""Register a CJK font with reportlab. Returns the font name to use."""
|
||||
global _REGISTERED, CJK_FONT
|
||||
if _REGISTERED:
|
||||
return CJK_FONT
|
||||
|
||||
# WenQuanYi Micro Hei: TTC with TrueType outlines (reportlab compatible)
|
||||
# subfontIndex 0 = regular
|
||||
candidates = [
|
||||
"/usr/share/fonts/wenquanyi/wqy-microhei/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",
|
||||
]
|
||||
|
||||
for path in candidates:
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont("WenQuanYi", path, subfontIndex=0))
|
||||
CJK_FONT = "WenQuanYi"
|
||||
_REGISTERED = True
|
||||
return CJK_FONT
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
print("WARNING: No CJK font available, Chinese text may not render correctly")
|
||||
_REGISTERED = True
|
||||
return CJK_FONT
|
||||
|
||||
|
||||
def _has_cjk(text: str) -> bool:
|
||||
"""Check if text contains CJK characters."""
|
||||
for ch in text:
|
||||
cp = ord(ch)
|
||||
if (
|
||||
0x4E00 <= cp <= 0x9FFF # CJK Unified Ideographs
|
||||
or 0x3400 <= cp <= 0x4DBF # CJK Extension A
|
||||
or 0x3000 <= cp <= 0x303F # CJK Symbols
|
||||
or 0xFF00 <= cp <= 0xFFEF # Fullwidth Forms
|
||||
or 0x2E80 <= cp <= 0x2EFF # CJK Radicals
|
||||
or 0xF900 <= cp <= 0xFAFF # CJK Compatibility
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def draw_cjk_text(c, x, y, text, font_name="Helvetica", font_size=10):
|
||||
"""Draw text, switching to CJK font if needed."""
|
||||
cjk_font = register_cjk_font()
|
||||
if _has_cjk(text) and cjk_font != "Helvetica":
|
||||
c.setFont(cjk_font, font_size)
|
||||
else:
|
||||
c.setFont(font_name, font_size)
|
||||
c.drawString(x, y, text)
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Generate DOCX: Letter TO client from Jingtian & Gongcheng."""
|
||||
|
||||
from pathlib import Path
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
|
||||
|
||||
def generate(
|
||||
output_path: Path,
|
||||
firm: dict,
|
||||
client: dict,
|
||||
body: str,
|
||||
closing: str,
|
||||
ref_number: str,
|
||||
date: str,
|
||||
re_line: str,
|
||||
):
|
||||
"""Generate a formal letter from the firm to a client."""
|
||||
doc = Document()
|
||||
|
||||
style = doc.styles["Normal"]
|
||||
font = style.font
|
||||
font.name = "Times New Roman"
|
||||
font.size = Pt(11)
|
||||
|
||||
# Firm letterhead
|
||||
header = doc.add_paragraph()
|
||||
header.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = header.add_run(firm["name"])
|
||||
run.bold = True
|
||||
run.font.size = Pt(14)
|
||||
run.font.color.rgb = RGBColor(0, 51, 102)
|
||||
|
||||
subheader = doc.add_paragraph()
|
||||
subheader.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = subheader.add_run(firm.get("chinese_name", ""))
|
||||
run.font.size = Pt(12)
|
||||
run.font.color.rgb = RGBColor(0, 51, 102)
|
||||
|
||||
addr = doc.add_paragraph()
|
||||
addr.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = addr.add_run(firm["address"])
|
||||
run.font.size = Pt(9)
|
||||
run.font.color.rgb = RGBColor(100, 100, 100)
|
||||
|
||||
# Divider line
|
||||
doc.add_paragraph("_" * 72).runs[0].font.color.rgb = RGBColor(180, 180, 180)
|
||||
|
||||
# Reference and date
|
||||
ref_para = doc.add_paragraph()
|
||||
ref_para.add_run(f"Our Ref: {ref_number}").font.size = Pt(10)
|
||||
date_para = doc.add_paragraph()
|
||||
date_para.add_run(f"Date: {date}").font.size = Pt(10)
|
||||
|
||||
doc.add_paragraph() # spacer
|
||||
|
||||
# Recipient
|
||||
recipient = doc.add_paragraph()
|
||||
recipient.add_run(f"{client['contact_person']}").font.size = Pt(11)
|
||||
recipient.add_run("\n")
|
||||
recipient.add_run(f"{client['name']}").font.size = Pt(11)
|
||||
recipient.add_run("\n")
|
||||
recipient.add_run(client["address"]).font.size = Pt(11)
|
||||
|
||||
doc.add_paragraph()
|
||||
|
||||
# RE line
|
||||
re_para = doc.add_paragraph()
|
||||
run = re_para.add_run(f"RE: {re_line}")
|
||||
run.bold = True
|
||||
run.underline = True
|
||||
|
||||
doc.add_paragraph()
|
||||
|
||||
# Body paragraphs — strip any closing already in body text
|
||||
body_clean = body
|
||||
for strip_phrase in [
|
||||
"Yours sincerely,",
|
||||
"Yours faithfully,",
|
||||
"Kind regards,",
|
||||
"Best regards,",
|
||||
]:
|
||||
if strip_phrase in body_clean:
|
||||
body_clean = body_clean[: body_clean.index(strip_phrase)].rstrip()
|
||||
|
||||
for para_text in body_clean.split("\n\n"):
|
||||
if para_text.strip():
|
||||
doc.add_paragraph(para_text.strip())
|
||||
|
||||
doc.add_paragraph()
|
||||
|
||||
# Signature block
|
||||
doc.add_paragraph("Yours faithfully,")
|
||||
doc.add_paragraph()
|
||||
sig = doc.add_paragraph()
|
||||
run = sig.add_run(firm["attorney"]["name"])
|
||||
run.bold = True
|
||||
doc.add_paragraph("Partner")
|
||||
doc.add_paragraph(firm["name"])
|
||||
|
||||
doc.save(str(output_path))
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Generate DOCX: Internal memo."""
|
||||
|
||||
from pathlib import Path
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
|
||||
|
||||
def generate(output_path: Path, firm: dict, body: str, date: str, subject: str):
|
||||
"""Generate an internal memo document."""
|
||||
doc = Document()
|
||||
|
||||
style = doc.styles["Normal"]
|
||||
font = style.font
|
||||
font.name = "Arial"
|
||||
font.size = Pt(11)
|
||||
|
||||
# MEMO header
|
||||
title = doc.add_paragraph()
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = title.add_run("INTERNAL MEMORANDUM")
|
||||
run.bold = True
|
||||
run.font.size = Pt(16)
|
||||
run.font.color.rgb = RGBColor(0, 51, 102)
|
||||
|
||||
# Confidential marker
|
||||
conf = doc.add_paragraph()
|
||||
conf.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = conf.add_run("CONFIDENTIAL")
|
||||
run.font.size = Pt(9)
|
||||
run.font.color.rgb = RGBColor(200, 0, 0)
|
||||
|
||||
doc.add_paragraph("_" * 60).runs[0].font.color.rgb = RGBColor(180, 180, 180)
|
||||
|
||||
# Memo fields
|
||||
fields = [
|
||||
("TO:", firm["attorney"]["name"]),
|
||||
("FROM:", firm["attorney"]["name"]),
|
||||
("DATE:", date),
|
||||
("RE:", subject),
|
||||
]
|
||||
|
||||
for label, value in fields:
|
||||
p = doc.add_paragraph()
|
||||
run_label = p.add_run(f"{label}\t")
|
||||
run_label.bold = True
|
||||
run_label.font.size = Pt(11)
|
||||
run_value = p.add_run(value)
|
||||
run_value.font.size = Pt(11)
|
||||
|
||||
doc.add_paragraph("_" * 60).runs[0].font.color.rgb = RGBColor(180, 180, 180)
|
||||
doc.add_paragraph()
|
||||
|
||||
# Body
|
||||
for para_text in body.split("\n\n"):
|
||||
if para_text.strip():
|
||||
doc.add_paragraph(para_text.strip())
|
||||
|
||||
doc.add_paragraph()
|
||||
|
||||
# Sign-off
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(firm["attorney"]["name"])
|
||||
run.bold = True
|
||||
doc.add_paragraph("Partner")
|
||||
doc.add_paragraph(firm["name"])
|
||||
|
||||
doc.save(str(output_path))
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Generate PNG: Email screenshot from client (Outlook-style)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
HAS_PIL = True
|
||||
except ImportError:
|
||||
HAS_PIL = False
|
||||
|
||||
|
||||
def _get_font(size, bold=False):
|
||||
"""Get a font with CJK support, falling back to default if unavailable."""
|
||||
# Prefer WenQuanYi Micro Hei for CJK support
|
||||
cjk_names = [
|
||||
"/usr/share/fonts/wenquanyi/wqy-microhei/wqy-microhei.ttc",
|
||||
]
|
||||
latin_names = [
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
if bold
|
||||
else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
|
||||
if bold
|
||||
else "/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
]
|
||||
# Try CJK font first (it includes Latin glyphs too)
|
||||
for name in cjk_names:
|
||||
try:
|
||||
return ImageFont.truetype(name, size, index=0)
|
||||
except (OSError, IOError):
|
||||
continue
|
||||
# Fall back to Latin fonts
|
||||
for name in latin_names:
|
||||
try:
|
||||
return ImageFont.truetype(name, size)
|
||||
except (OSError, IOError):
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def generate(
|
||||
output_path: Path,
|
||||
from_email: str,
|
||||
from_name: str,
|
||||
to_email: str,
|
||||
to_name: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
date: str,
|
||||
):
|
||||
"""Generate an Outlook-style email screenshot as PNG."""
|
||||
if not HAS_PIL:
|
||||
# Fallback: write a text file
|
||||
output_path.with_suffix(".txt").write_text(
|
||||
f"From: {from_name} <{from_email}>\n"
|
||||
f"To: {to_name} <{to_email}>\n"
|
||||
f"Date: {date}\n"
|
||||
f"Subject: {subject}\n\n{body}"
|
||||
)
|
||||
return
|
||||
|
||||
# Image dimensions (simulate a screen capture)
|
||||
width, height = 800, 600
|
||||
bg_color = (255, 255, 255)
|
||||
header_bg = (242, 242, 242)
|
||||
accent_color = (0, 120, 212) # Outlook blue
|
||||
text_color = (51, 51, 51)
|
||||
label_color = (130, 130, 130)
|
||||
|
||||
img = Image.new("RGB", (width, height), bg_color)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
font_header = _get_font(11, bold=True)
|
||||
font_label = _get_font(10)
|
||||
font_body = _get_font(11)
|
||||
font_subject = _get_font(13, bold=True)
|
||||
|
||||
# Top bar (Outlook-style)
|
||||
draw.rectangle([0, 0, width, 45], fill=accent_color)
|
||||
draw.text((15, 12), "Mail - Outlook", fill=(255, 255, 255), font=font_header)
|
||||
|
||||
# Email header area
|
||||
y = 55
|
||||
draw.rectangle([0, 45, width, 200], fill=header_bg)
|
||||
|
||||
# Subject
|
||||
draw.text((20, y), subject, fill=text_color, font=font_subject)
|
||||
y += 30
|
||||
|
||||
# From
|
||||
draw.text((20, y), "From:", fill=label_color, font=font_label)
|
||||
draw.text((80, y), f"{from_name} <{from_email}>", fill=text_color, font=font_body)
|
||||
y += 22
|
||||
|
||||
# Sent
|
||||
draw.text((20, y), "Sent:", fill=label_color, font=font_label)
|
||||
draw.text((80, y), date, fill=text_color, font=font_body)
|
||||
y += 22
|
||||
|
||||
# To
|
||||
draw.text((20, y), "To:", fill=label_color, font=font_label)
|
||||
draw.text((80, y), f"{to_name} <{to_email}>", fill=text_color, font=font_body)
|
||||
y += 22
|
||||
|
||||
# Subject line in header
|
||||
draw.text((20, y), "Subject:", fill=label_color, font=font_label)
|
||||
draw.text((80, y), subject[:60], fill=text_color, font=font_body)
|
||||
y += 30
|
||||
|
||||
# Divider
|
||||
draw.line([(15, y), (width - 15, y)], fill=(220, 220, 220), width=1)
|
||||
y += 15
|
||||
|
||||
# Body text
|
||||
for para in body.split("\n\n"):
|
||||
for line in _wrap_text(para.strip(), 90):
|
||||
if y > height - 30:
|
||||
break
|
||||
draw.text((25, y), line, fill=text_color, font=font_body)
|
||||
y += 18
|
||||
y += 8
|
||||
|
||||
img.save(str(output_path), "PNG")
|
||||
|
||||
|
||||
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 or [""]
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Generate PDF: IPD Filing Receipt (native text PDF, bilingual)."""
|
||||
|
||||
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
|
||||
from generators.cjk_font import register_cjk_font
|
||||
|
||||
|
||||
def _set_font(c, bold=False, size=10):
|
||||
"""Set font with CJK support."""
|
||||
cjk = register_cjk_font()
|
||||
if cjk != "Helvetica":
|
||||
c.setFont(cjk, size)
|
||||
else:
|
||||
c.setFont("Helvetica-Bold" if bold else "Helvetica", size)
|
||||
|
||||
|
||||
def _has_cjk(text):
|
||||
"""Check if text contains CJK characters."""
|
||||
return any(ord(ch) > 0x2E80 for ch in text)
|
||||
|
||||
|
||||
def generate(
|
||||
output_path: Path,
|
||||
ipd: dict,
|
||||
tm_number: str,
|
||||
tm_text: str,
|
||||
applicant: str,
|
||||
applicant_address: str,
|
||||
agent: str,
|
||||
agent_address: str,
|
||||
filing_date: str,
|
||||
response_deadline: str,
|
||||
nice_class: str,
|
||||
class_description: str,
|
||||
):
|
||||
"""Generate an IPD filing receipt PDF."""
|
||||
cjk = register_cjk_font()
|
||||
c = canvas.Canvas(str(output_path), pagesize=A4)
|
||||
w, h = A4
|
||||
|
||||
def draw_text(x, y_pos, text, size=10, bold=False, centered=False):
|
||||
"""Draw text, auto-switching to CJK font if needed."""
|
||||
if _has_cjk(text) and cjk != "Helvetica":
|
||||
c.setFont(cjk, size)
|
||||
else:
|
||||
c.setFont("Helvetica-Bold" if bold else "Helvetica", size)
|
||||
if centered:
|
||||
c.drawCentredString(x, y_pos, text)
|
||||
else:
|
||||
c.drawString(x, y_pos, text)
|
||||
|
||||
# Header
|
||||
draw_text(w / 2, h - 50, ipd["name"], 12, bold=True, centered=True)
|
||||
draw_text(w / 2, h - 66, ipd["chinese_name"], 10, centered=True)
|
||||
draw_text(
|
||||
w / 2,
|
||||
h - 82,
|
||||
"The Government of the Hong Kong Special Administrative Region",
|
||||
10,
|
||||
centered=True,
|
||||
)
|
||||
|
||||
# Divider
|
||||
c.setStrokeColor(HexColor("#333333"))
|
||||
c.setLineWidth(1)
|
||||
c.line(50, h - 95, w - 50, h - 95)
|
||||
|
||||
# Title
|
||||
draw_text(
|
||||
w / 2,
|
||||
h - 120,
|
||||
"E-Filing Receipt / Acknowledgment",
|
||||
14,
|
||||
bold=True,
|
||||
centered=True,
|
||||
)
|
||||
|
||||
# Receipt details
|
||||
y = h - 160
|
||||
fields = [
|
||||
("Receipt No.:", f"EF-{tm_number[-6:]}"),
|
||||
("Application No. / 申請編號:", tm_number),
|
||||
("Trade Mark Text / 商標文字:", tm_text),
|
||||
("Mark Type / 商標種類:", "Ordinary"),
|
||||
("Class No. / 類別編號:", nice_class),
|
||||
("Specification / 貨品/服務說明:", class_description),
|
||||
("", ""),
|
||||
("Applicant / 申請人:", applicant),
|
||||
("Address / 地址:", applicant_address),
|
||||
("", ""),
|
||||
("Agent / 代理人:", agent),
|
||||
("Agent Address / 代理人地址:", agent_address),
|
||||
("", ""),
|
||||
("Date of Filing / 提交日期:", filing_date),
|
||||
("Date of Receipt / 確認日期:", filing_date),
|
||||
]
|
||||
|
||||
for label, value in fields:
|
||||
if not label and not value:
|
||||
y -= 10
|
||||
continue
|
||||
draw_text(50, y, label, 10, bold=True)
|
||||
# Wrap long values
|
||||
if len(value) > 60:
|
||||
draw_text(250, y, value[:60], 10)
|
||||
y -= 16
|
||||
draw_text(250, y, value[60:120], 10)
|
||||
else:
|
||||
draw_text(250, y, value, 10)
|
||||
y -= 18
|
||||
|
||||
# Response deadline (highlighted)
|
||||
y -= 15
|
||||
c.setStrokeColor(HexColor("#cc0000"))
|
||||
c.setLineWidth(0.5)
|
||||
c.rect(40, y - 10, w - 80, 40, stroke=1, fill=0)
|
||||
|
||||
c.setFillColor(HexColor("#cc0000"))
|
||||
draw_text(50, y + 12, "IMPORTANT / 重要通知:", 11, bold=True)
|
||||
c.setFillColor(HexColor("#000000"))
|
||||
draw_text(50, y - 4, f"Response deadline / 回覆限期: {response_deadline}", 10)
|
||||
|
||||
# Footer
|
||||
y -= 50
|
||||
c.setFillColor(HexColor("#666666"))
|
||||
draw_text(
|
||||
w / 2,
|
||||
y,
|
||||
"This is a computer-generated receipt. No signature is required.",
|
||||
8,
|
||||
centered=True,
|
||||
)
|
||||
draw_text(w / 2, y - 14, "此為電腦自動產生之收據,毋須簽署。", 8, centered=True)
|
||||
|
||||
c.save()
|
||||
@@ -0,0 +1,144 @@
|
||||
"""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())
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Generate PDF: Trademark registry record (mimics HK IPD format)."""
|
||||
|
||||
from pathlib import Path
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.colors import HexColor
|
||||
|
||||
from .cjk_font import register_cjk_font, _has_cjk
|
||||
|
||||
|
||||
def generate(
|
||||
output_path: Path,
|
||||
ipd: dict,
|
||||
tm_number: str,
|
||||
tm_text: str,
|
||||
status: str,
|
||||
nice_class: str,
|
||||
class_description: str,
|
||||
applicant: str,
|
||||
applicant_address: str,
|
||||
agent: str,
|
||||
agent_address: str,
|
||||
filing_date: str,
|
||||
publication_date: str,
|
||||
):
|
||||
"""Generate a TM registry record PDF (mimicking 306457384.pdf)."""
|
||||
cjk = register_cjk_font()
|
||||
c = canvas.Canvas(str(output_path), pagesize=A4)
|
||||
w, h = A4
|
||||
|
||||
def draw_text(x, y_pos, text, size=10, bold=False, centered=False):
|
||||
if _has_cjk(text) and cjk != "Helvetica":
|
||||
c.setFont(cjk, size)
|
||||
else:
|
||||
c.setFont("Helvetica-Bold" if bold else "Helvetica", size)
|
||||
if centered:
|
||||
c.drawCentredString(x, y_pos, text)
|
||||
else:
|
||||
c.drawString(x, y_pos, text)
|
||||
|
||||
# ── Page 1: Basic Information ──────────────────────────────────
|
||||
|
||||
# Header
|
||||
draw_text(w / 2, h - 45, ipd["chinese_name"], 11, bold=True, centered=True)
|
||||
draw_text(w / 2, h - 60, ipd["name"], 10, centered=True)
|
||||
draw_text(
|
||||
w / 2,
|
||||
h - 75,
|
||||
"The Government of the Hong Kong Special Administrative Region",
|
||||
9,
|
||||
centered=True,
|
||||
)
|
||||
|
||||
# Divider
|
||||
c.setStrokeColor(HexColor("#333333"))
|
||||
c.line(50, h - 88, w - 50, h - 88)
|
||||
|
||||
# Titles
|
||||
draw_text(w / 2, h - 108, "商標記錄", 12, bold=True, centered=True)
|
||||
draw_text(w / 2, h - 124, "Trade Mark Records", 12, bold=True, centered=True)
|
||||
|
||||
# Section: Basic Information
|
||||
y = h - 150
|
||||
draw_text(50, y, "基本資料 Basic information", 10, bold=True)
|
||||
y -= 5
|
||||
c.line(50, y, w - 50, y)
|
||||
y -= 20
|
||||
|
||||
value_x = 280
|
||||
|
||||
fields_page1 = [
|
||||
("[210/111]", "商標編號:\nTrade Mark No.:", tm_number),
|
||||
("", "狀況:\nStatus:", status),
|
||||
("", "商標文字:\nTrade Mark Text:", tm_text),
|
||||
("[550]", "商標種類:\nMark Type:", "Ordinary"),
|
||||
("[511]", "類別編號:\nClass No.:", nice_class),
|
||||
("[511]", "貨品 / 服務說明:\nSpecification:", class_description),
|
||||
]
|
||||
|
||||
for code, label, value in fields_page1:
|
||||
if code:
|
||||
c.setFont("Helvetica", 8)
|
||||
c.drawString(50, y, code)
|
||||
|
||||
# Draw bilingual label
|
||||
label_lines = label.split("\n")
|
||||
for i, ll in enumerate(label_lines):
|
||||
draw_text(105, y - (i * 14), ll, 9, bold=True)
|
||||
|
||||
# Draw value
|
||||
draw_text(value_x, y, value, 10)
|
||||
if len(value) > 55:
|
||||
# Wrap
|
||||
draw_text(value_x, y, value[:55], 10)
|
||||
y -= 16
|
||||
draw_text(value_x, y, value[55:110], 10)
|
||||
|
||||
y -= max(len(label_lines) * 14, 18) + 8
|
||||
|
||||
# Dates section
|
||||
y -= 10
|
||||
draw_text(50, y, "日期 (日日-月月-年年年年)", 10, bold=True)
|
||||
c.setFont("Helvetica-Bold", 10)
|
||||
c.drawString(50, y - 14, "Dates (DD-MM-YYYY)")
|
||||
y -= 19
|
||||
c.line(50, y, w - 50, y)
|
||||
y -= 20
|
||||
|
||||
date_fields = [
|
||||
("[220]", "提交日期:\nDate of Filing:", filing_date),
|
||||
("[442]", "公布獲接納註冊申請日期:\nDate of Publication:", publication_date),
|
||||
]
|
||||
|
||||
for code, label, value in date_fields:
|
||||
c.setFont("Helvetica", 8)
|
||||
c.drawString(50, y, code)
|
||||
label_lines = label.split("\n")
|
||||
for i, ll in enumerate(label_lines):
|
||||
draw_text(105, y - (i * 14), ll, 9, bold=True)
|
||||
draw_text(value_x, y, value, 10)
|
||||
y -= max(len(label_lines) * 14, 18) + 8
|
||||
|
||||
# ── Page 2: Applicant/Owner ────────────────────────────────────
|
||||
c.showPage()
|
||||
y = h - 60
|
||||
|
||||
draw_text(50, y, "申請人/擁有人", 10, bold=True)
|
||||
c.setFont("Helvetica-Bold", 10)
|
||||
c.drawString(50, y - 14, "Applicant/Owner")
|
||||
y -= 19
|
||||
c.line(50, y, w - 50, y)
|
||||
y -= 20
|
||||
|
||||
owner_fields = [
|
||||
("[730]", "姓名/名稱:\nName:", applicant),
|
||||
("[730]", "地址:\nAddress:", applicant_address),
|
||||
("[842]", "類別:\nType:", "Incorporated"),
|
||||
("[842]", "公司成立為法團的所在地:\nPlace of Incorporation:", "HONG KONG"),
|
||||
(
|
||||
"[750]",
|
||||
"供送達文件的地址:\nAddress for Service:",
|
||||
f"{agent}\n{agent_address}",
|
||||
),
|
||||
("[740]", "代理人詳情:\nAgent's Details:", f"{agent}\n{agent_address}"),
|
||||
]
|
||||
|
||||
for code, label, value in owner_fields:
|
||||
c.setFont("Helvetica", 8)
|
||||
c.drawString(50, y, code)
|
||||
|
||||
label_lines = label.split("\n")
|
||||
for i, ll in enumerate(label_lines):
|
||||
draw_text(105, y - (i * 14), ll, 9, bold=True)
|
||||
|
||||
value_lines = value.split("\n")
|
||||
for i, vl in enumerate(value_lines):
|
||||
if len(vl) > 50:
|
||||
draw_text(value_x, y - (i * 14), vl[:50], 10)
|
||||
draw_text(value_x, y - ((i + 1) * 14), vl[50:100], 10)
|
||||
else:
|
||||
draw_text(value_x, y - (i * 14), vl, 10)
|
||||
|
||||
y -= max(len(label_lines), len(value_lines)) * 14 + 12
|
||||
|
||||
c.save()
|
||||
@@ -0,0 +1,111 @@
|
||||
"""Generate XLSX: Invoice/billing schedule."""
|
||||
|
||||
from pathlib import Path
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
|
||||
def generate(output_path: Path, rows: list, firm: dict):
|
||||
"""Generate an invoice schedule Excel file."""
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Invoice Schedule"
|
||||
|
||||
# Styles
|
||||
header_font = Font(name="Arial", size=12, bold=True, color="003366")
|
||||
col_header_font = Font(name="Arial", size=10, bold=True, color="FFFFFF")
|
||||
col_header_fill = PatternFill(
|
||||
start_color="003366", end_color="003366", fill_type="solid"
|
||||
)
|
||||
data_font = Font(name="Arial", size=10)
|
||||
currency_format = "#,##0"
|
||||
thin_border = Border(
|
||||
left=Side(style="thin"),
|
||||
right=Side(style="thin"),
|
||||
top=Side(style="thin"),
|
||||
bottom=Side(style="thin"),
|
||||
)
|
||||
|
||||
# Title
|
||||
ws.merge_cells("A1:G1")
|
||||
ws["A1"] = f"{firm['name']} - Invoice Schedule"
|
||||
ws["A1"].font = header_font
|
||||
ws["A1"].alignment = Alignment(horizontal="center")
|
||||
|
||||
ws.merge_cells("A2:G2")
|
||||
ws["A2"] = firm["address"]
|
||||
ws["A2"].font = Font(name="Arial", size=8, color="666666")
|
||||
ws["A2"].alignment = Alignment(horizontal="center")
|
||||
|
||||
# Column headers
|
||||
headers = [
|
||||
"Client",
|
||||
"Matter Ref",
|
||||
"TM Number",
|
||||
"Description",
|
||||
"Amount (HKD)",
|
||||
"Due Date",
|
||||
"Status",
|
||||
]
|
||||
col_widths = [25, 15, 14, 40, 15, 14, 12]
|
||||
|
||||
for i, (header, width) in enumerate(zip(headers, col_widths), start=1):
|
||||
cell = ws.cell(row=4, column=i, value=header)
|
||||
cell.font = col_header_font
|
||||
cell.fill = col_header_fill
|
||||
cell.alignment = Alignment(horizontal="center")
|
||||
cell.border = thin_border
|
||||
ws.column_dimensions[get_column_letter(i)].width = width
|
||||
|
||||
# Data rows
|
||||
for row_idx, row_data in enumerate(rows, start=5):
|
||||
ws.cell(row=row_idx, column=1, value=row_data["client"]).font = data_font
|
||||
ws.cell(row=row_idx, column=2, value=row_data["matter_ref"]).font = data_font
|
||||
ws.cell(row=row_idx, column=3, value=row_data["tm_number"]).font = data_font
|
||||
ws.cell(row=row_idx, column=4, value=row_data["description"]).font = data_font
|
||||
|
||||
amount_cell = ws.cell(row=row_idx, column=5, value=row_data["amount_hkd"])
|
||||
amount_cell.font = data_font
|
||||
amount_cell.number_format = currency_format
|
||||
amount_cell.alignment = Alignment(horizontal="right")
|
||||
|
||||
ws.cell(row=row_idx, column=6, value=row_data["due_date"]).font = data_font
|
||||
|
||||
status_cell = ws.cell(row=row_idx, column=7, value=row_data["status"])
|
||||
status_cell.font = data_font
|
||||
status_cell.alignment = Alignment(horizontal="center")
|
||||
|
||||
# Color-code status
|
||||
status_colors = {
|
||||
"Draft": "FFF3CD",
|
||||
"Sent": "D1ECF1",
|
||||
"Overdue": "F8D7DA",
|
||||
"Paid": "D4EDDA",
|
||||
}
|
||||
if row_data["status"] in status_colors:
|
||||
status_cell.fill = PatternFill(
|
||||
start_color=status_colors[row_data["status"]],
|
||||
end_color=status_colors[row_data["status"]],
|
||||
fill_type="solid",
|
||||
)
|
||||
|
||||
# Apply borders
|
||||
for col in range(1, 8):
|
||||
ws.cell(row=row_idx, column=col).border = thin_border
|
||||
|
||||
# Total row
|
||||
total_row = 5 + len(rows)
|
||||
ws.cell(row=total_row, column=4, value="TOTAL").font = Font(
|
||||
name="Arial", size=10, bold=True
|
||||
)
|
||||
total_cell = ws.cell(
|
||||
row=total_row,
|
||||
column=5,
|
||||
value=sum(r["amount_hkd"] for r in rows),
|
||||
)
|
||||
total_cell.font = Font(name="Arial", size=10, bold=True)
|
||||
total_cell.number_format = currency_format
|
||||
total_cell.alignment = Alignment(horizontal="right")
|
||||
|
||||
wb.save(str(output_path))
|
||||
Reference in New Issue
Block a user