Chinese Professional PDF Report Generation
🚨 Lessons Learned from Two User Corrections (2026-08-20, Highest Priority, Do Not Repeat)
- Compact Layout: Strictly prohibited from using "a page for just a little content". Do not add PageBreaks to pad page count for each chapter — only add one PageBreak after the cover; let the body flow naturally. A previous 6-page report must be compressed to 4 pages.
- Large Font Sizes (Mobile-Readable, Client Reads on Phone): Body text ≥16pt, tables ≥15pt, H2 ≥17pt, H1 ≥21pt, title ≥30pt. 14pt body / 12pt table is too small; the client cannot read on their phone, and the user will criticize on the spot.
- These two rules are also written in the memory layout standard, but you still tend to be lazy and use 14pt/12pt + page break per chapter — you must self-check the font size constants and PageBreak count before generating the script.
⭐ Army-Wide Layout Standard (2026-08-18, User-Approved, Universal for PDF/Word)
This layout logic is the standard; all future PDF and Word documents must follow it:
- A4 Portrait (210×297mm) — do not use landscape or other sizes
- Large Font Sizes, Mobile-Readable: Body text ≥14pt, tables ≥12.5pt, H1 ≥20pt, title ≥32pt
- Tables Wrap Smartly Without Overflow: Use Paragraph +
wordWrap="CJK"for cells - Balanced Page Content: Do not force page breaks; use KeepTogether to intelligently chunk content. After generation, check the character count per page (except cover and ending pages; all pages should be balanced)
- Compact Layout: Margins 18-20mm, table padding 3-4, tight line spacing, ready for direct printing
Word documents follow the same logic: A4 portrait, body text ≥Xiao Si/Size 4 (12-14pt), table columns auto-fit without overflow, balanced pages.
Trigger Conditions
- User/client requests: "make conclusions into PDF", "send PDF", "give me the feasibility study", "render conclusions and send"
- Need to deliver professionally typeset Chinese analysis reports (cover + tables + footer + key conclusions box)
- Reference example: Guoxun Robot Due Diligence Report (5 pages, 02-Project Results/Guoxun Robot Project Feasibility Due Diligence and Analysis Report.pdf)
Environment & Installation
- Use hermes venv interpreter:
python - First-time install:
$V -m pip install reportlab(already installed: 5.0.0) - Fonts: Windows system comes with Microsoft YaHei, no need to download font files
C:/Windows/Fonts/msyh.ttc(Regular, subfontIndex=0)C:/Windows/Fonts/msyhbd.ttc(Bold)C:/Windows/Fonts/msyhl.ttc(Light)
Standard Workflow
- ⚠️ Delivery Iron Rule (2026-09-02, User-Stated, Highest Priority): After generating the PDF, you must send it to the Feishu chat via
MEDIA:, never just save it to the knowledge base / give a path / send bullet points. The user's exact words: "You must send the PDF document to the Feishu chat here, otherwise how can I see it? This must become an iron rule."- The user reads on their phone; they can only see the file by clicking the file in the Feishu message. Saving it to
02-Project Results/is just a backup, not a delivery. - Delivery format:
<your output directory>/<filename>.pdf(absolute path). - After sending the PDF, attach a 30-second version of the key conclusions. Send Feishu first, then save to KB for review.
- The user reads on their phone; they can only see the file by clicking the file in the Feishu message. Saving it to
- Get Content: Use
session_searchto look up historical analysis — the user often replies to attachments with "make conclusions into PDF", first retrieve the previous research/due diligence content, do not rewrite - Write Script: Copy
templates/reportlab_chinese_template.py, only replace the content area (cover text, chapters, table data) - Generate: Run python → output to Obsidian
Hermes Work Results/02-Project Results/(naming:<topic> Project Feasibility Due Diligence and Analysis Report.pdf) - Validate: Use pymupdf to open and read
get_text, confirm page count / Chinese rendering / existence of final page conclusions - Deliver: Reply with
MEDIA:D:\...pdfpath + key conclusions (30-second version), no verbosity
⭐ Color Palette Iron Rule (2026-09-02, User-Approved, Auspicious Colors are Gold/Earth — Highest Priority)
The user's Bazi favors gold and earth, and dislikes water (blue/black) and fire (red/purple). All PDFs must use gold/earth color schemes; blue is prohibited. See color variables in palette below.
- Chapter Title H1: Metallic gold background + white text (
backColor=#C9A227,textColor=white,borderPadding=(6,8,6,8)) — Gold background with white text, not blue text. - Table Header: Metallic gold background + white text (
header_bg=#C9A227,FONTNAME=MSYHBD) — Gold background with white text. - Body Text: Pure black
#1f1f1f. - Subtitle / Dividing Line: Deep earth gold
#B8860B(remove red). - Code Blocks: Earth gold
#8a6d1f(remove blue). - Zebra Stripes: Light beige gold
#FBF3DE(warm tone, not gray-white). - Grid Lines: Light earth gold
#D9C9A3.
Gold-Tone Palette (for ReportLab)
C_DARK=colors.HexColor("#1f1f1f") # Body text near black
C_RED=colors.HexColor("#B8860B") # Emphasis/subtitle - deep earth gold
C_BLUE=colors.HexColor("#C9A227") # Table header/title background - metallic gold
C_LGRAY=colors.HexColor("#FBF3DE") # Zebra stripes - light beige gold
C_MGRAY=colors.HexColor("#D9C9A3") # Grid lines - light earth gold
C_WHITE=colors.white
Inline emphasis
S_CODE=st(..., textColor=colors.HexColor("#8a6d1f")); inline<span color='#B8860B'>(do not use#c0392bred).
Layout Points (Template Already Encapsulated)
- Cover Page: Large title + gold subtitle + date/source + key conclusions box (deep earth gold background, white text table)
- Body: H1 chapters (metallic gold background, white text, MSYHBD 21pt) + H2 subsections (17pt) + body text 15pt (2026-08-18 user instruction: mobile-readable, font size must be large)
- Tables:
make_table()header with metallic gold background, white text + zebra stripes (ROWBACKGROUNDS) + grid + repeatRows=1, table font size 13pt - Footer: onPage callback to draw report name + page number
- Emphasis: Paragraph supports
<b>/<span color='#B8860B'>inline markers - Long reports by chapter: Use PageBreak to separate cover and body
⚠️ Font Size Iron Rule (2026-08-18, User-Emphasized): PDFs are for mobile viewing: body text ≥15pt, tables ≥13pt, H1 ≥21pt, title ≥34pt. The original template at 10.5pt/9.3pt is too small; mobile screens cannot read clearly — all new reports must use large font sizes. Larger font sizes may increase page count (e.g., 5 pages to 8 pages); this is normal.
⭐ Smart Layout Iron Rule (2026-08-18, User-Emphasized, Print-Friendly) Page content must be balanced — a page cannot have only 200 characters while another has 800. Method:
- Use PageBreak Sparingly: Only force a page break after the cover; let the body flow naturally
- Wrap "Heading + Content" with KeepTogether: Use
keep(H2 heading, table/paragraph)to prevent tables from being split across two pages, and to prevent headings from being orphaned at the page bottom - Cover is a Standalone Page: Cover elements + one PageBreak
- Use Subsections for Natural Separation: H2 headings + tables/key points, do not force a page break per chapter
- Verify Balance: After generation, use pymupdf to check the character count per page — for a normal report, except cover/ending pages, all pages should have similar character counts (e.g., 600-900 characters). If the difference is too large, adjust
- Compact Layout: Margins 20mm/16mm (not too large), table padding 4-5, line spacing leading=22 (14pt body), reduce wasted whitespace
Pitfalls
- ⛔ Prohibit Justified Alignment (TA_JUSTIFY) — Character spacing gets stretched (2026-09-03, user's top concern for mobile readability): ReportLab's
TA_JUSTIFYforces each line of a Chinese paragraph to stretch to the right edge; when a line has few characters, the spacing is stretched extremely wide and looks terrible. All body text/lists/paragraphs must useTA_LEFTleft alignment.- Wrong:
b=dict(..., alignment=TA_JUSTIFY, ...) - Correct:
b=dict(..., alignment=TA_LEFT, ...) - Check: If a body/list style inherits from
st()and hasalignment=TA_JUSTIFY, it must be changed to left alignment.
- Wrong:
- ⛔ Cover Content Overflows into a Blank Orphan Page (2026-09-03): All cover elements (main title + subtitle + description + one-sentence key point) must fit entirely on page 1. Never let cover content be pushed to page 2 forming "a small paragraph of text + large blank space". If cover content is too much, delete long introductions, compress list spacing to fit it all on one page. Keep only one PageBreak after the cover; body flows naturally.
- ⛔ Font Size and Margins Must Be Adjusted Together: After increasing font size, margins must be reduced proportionally (in this session, 22mm → 17mm) to fit more content on each page and avoid orphan pages. 17pt body + 17mm margins is the empirically tested balance point; table font size 14.5pt.
- ⛔ Wrapping "Custom-Aligned Small Tables" in KeepTogether Causes Page Height Miscalculation → Orphan Pages (2026-09-06, Tested): Wrapping "heading + body + custom two-column aligned table (field_table) + price" in KeepTogether leads to inaccurate height estimation by ReportLab, often pushing the entire block to the next page, leaving only a "prompt sentence/last row" on the previous page, creating an orphan page with dozens of characters (in this session, page 2 had only 76 characters). Fix: ① For the "Service Overview Table + Introduction" on the first screen, wrap the entire head_block in KeepTogether and compress table font size/line spacing to ensure it fits on page 1; ② After rendering, use pymupdf to check the character count per page. Any page with <120 characters is an orphan page; immediately compress the previous page's table or remove redundant prompt sentences.
- Differentiation (2026-09-10, Tested): Wrapping only the "table itself" is accurate and should be done; wrapping "heading + body + table + price" as a large block is prone to miscalculation. Let the table factory function uniformly
return KeepTogether(t)(the table as a whole does not cross pages), and combine with keeping only one PageBreak after the cover. This was tested to fix a 7-page report (page 5 had only 77 characters) into 6 pages with 301/451/548/407/551/240 characters per page. Tables being cut by page breaks, with 1–2 rows isolated on their own page, is the primary cause of orphan pages; if you find a page with only a table header/last row, check this first.
- Differentiation (2026-09-10, Tested): Wrapping only the "table itself" is accurate and should be done; wrapping "heading + body + table + price" as a large block is prone to miscalculation. Let the table factory function uniformly
- ⛔ The Label Column in a Two-Column "Label+Content" Alignment Table Should Not Be Too Narrow — Chinese Labels Will Wrap into Orphan Characters (2026-09-06): When using a two-column alignment table for "What can be done / How deep / What you get", if the label column is too narrow (e.g., 32mm), "What you get" will wrap into "What you ge" + "t", and the orphan character looks terrible. Fix: The label column width must be > the width required to fit the longest label on a single line (around 40mm), or shorten the label (e.g., "What you get" → "Benefits").
- ⛔ The period at the end of a long sentence can become an orphaned punctuation mark (2026-09-06): For long fields like "How deep / Benefits", the period at the end of the whole sentence gets pushed to the start of the next line when it wraps. Fix: When the field value is long and may wrap, remove the trailing period (or use a short phrase without a period); better to have no period than a lone dot.
- ⛔ "Custom Skills" Must Use the Correct Terminology (2026-09-06, User Correction): Do not mistakenly write "custom/made-to-order skills" as "scheduled tasks". The client-facing version should uniformly use "Custom Skill Tailoring" and explain in plain language that "a skill is a specific capability an AI can execute" (see enterprise-ai-service-blueprint).
.ttcfonts must usesubfontIndex=0, otherwise errors or garbled text- Bold must register MSYHBD and set fontName — the
<b>tag silently fails to bold if no bold font is registered - Escaping
<and>must handle two cases (2026-09-10, Tested and Got Burned): Only escape bare angle brackets in body text (e.g., "accuracy <10mm") to</>;<b>,<span color=...>are inline markers and must be passed to Paragraph as-is to work.- ⛔ Fatal approach: Always apply
.replace('<','<')to all cell text → bold in tables will display literally as<b>text(in this session, the entire second page column printed<b>Targeted support..., only discovered before delivery). - ✅ Correct: Table cells only escape bare
&(v.replace('&','&')), preserve inline tags; bare angle brackets in content should be written as<by the caller. - Self-check: Before delivery, search the full text; if
'<b>' in textor'</b>' in textis true, the tag was rendered as text. You must go back and checkmake_table's escaping.
- ⛔ Fatal approach: Always apply
- Sum of table column widths ≤ A4 usable width (210mm - left/right margins), otherwise overflow will cause ugly wrapping
- Table Chinese text overflow (2026-08-18, Important Fix): ReportLab's Table placed directly with long Chinese text will not wrap automatically and will overflow to the right! Must wrap cells with Paragraph +
wordWrap="CJK". This is built intomake_tablevia_cell_style()— all cells wrap automatically. Verification: Use pymupdf to checkget_text('words')forx2exceeding the content area's right edge.- The threshold must be calculated based on actual margins, not hardcoded as 540pt (2026-09-10, Tested): Right edge =
A4[0] - rightMargin(A4 width 595.3pt; 16mm margin → 549.9pt, 17mm → 547pt). Hardcoding 540pt will falsely report normal wrapping as overflow, causing wasted effort. - Must exclude footer: The "Page N" drawn by onPage always exceeds the content area. When making the judgment, filter out footer text (e.g.,
w[4] != 'page'), otherwise you'll get false alarms every time. - Judgment example:
[w for w in page.get_text('words') if w[2] > page.rect.width - rightMargin + 1 and w[4] != 'page']
- The threshold must be calculated based on actual margins, not hardcoded as 540pt (2026-09-10, Tested): Right edge =
- A4 Portrait Iron Rule (User-Emphasized): All PDFs must be A4 portrait (210×297mm).
pagesize=A4is portrait by default; do not change to landscape. - Before delivery, must verify once with pymupdf (page count / Chinese rendering / garbled text / no overflow) — only send if verification passes.
- ⛔ When writing the generation script, all Python strings must use single quotes for delimiters (2026-09-10, Tested): The toolchain normalizes Chinese quotation marks “ ” into half-width
". If the outer string uses double quotes as delimiters, the quotation marks in the body will prematurely close the literal, causing aSyntaxError: invalid character '+' (U+FF0B)or similar (the error point looks like an illegal full-width character, but the real cause is the quote truncation, which misleads debugging). Either use「」for Chinese quotation marks in the body, or wrap the entire string in'...'. - ⛔ Never "Read and Write the Same File" Simultaneously:
open(f,'w').write(open(f,encoding='utf-8').read() + add)will truncate the file to 0 bytes before reading it, resulting in losing all original content and only keeping the appended part (in this session, a knowledge base note went from 13KB to 3.8KB, almost delivering a broken file). When appending content, first storetxt = open(f).read()in a variable, thenopen(f,'w').write(txt + add); or use a patch tool. - Files >100KB are usually normal (embedded font subsets).
Verification Command
$V -c "import pymupdf; d=pymupdf.open(r'D:\...\report.pdf'); print(len(d)); print(d[0].get_text()[:120]); print('Final page contains conclusion:', 'Conclusion' in d[-1].get_text())"
Related SKILLs
- Content Framework:
feasibility-study-writing,dual-lens-business-analysis(load before outputting dual-lens research reports) - Report Review Iron Rule: Execute
report-reflection-reviewbefore finalizing rigorous reports - PPT Scenario:
ultimate-ppt-master - Template:
templates/reportlab_chinese_template.py(old);templates/reportlab_gold_template.py(recommended): Gold color scheme + left alignment + 17pt large font + 17mm compact margins + no orphan pages. Fixedmake_table()inline tag handling (<b>/<span>inside table cells now work correctly, no longer escaped as literals).