Install
openclaw skills install zcx-pdf-report-generatorConvert Markdown reports to professionally formatted PDF documents using pdfkit. Supports Chinese fonts, A4 layout, auto headers/footers, page numbers. Designed primarily for daily-market-report output but usable for any Markdown-to-PDF conversion.
openclaw skills install zcx-pdf-report-generatorConvert structured Markdown reports into print-ready A4 PDF documents with Chinese font support, automatic headers/footers, and page numbers. Designed to work hand-in-hand with the daily-market-report skill.
| Scenario | Trigger |
|---|---|
| End-of-day market report → PDF | After daily-market-report generates a Markdown summary |
| Trade journal / weekly review export | Convert structured notes to PDF |
| Investment memo generation | Produce clean A4 printouts for offline review |
| Any Markdown → formatted PDF | General-purpose conversion |
The pdfkit library is already installed at:
C:\Users\Tania\.openclaw\workspace\.tmp-report\node_modules\pdfkit
No additional installs needed. The skill uses this existing dependency.
For Chinese text rendering, pdfkit requires a TrueType font file. Recommended:
C:\Windows\Fonts\msyh.ttcPlace the font file in the skill's assets/ directory or reference it from a known path. Example:
# Option 1: Place in assets/
cp /path/to/NotoSansSC-Regular.ttf skills/pdf-report-generator/assets/
# Option 2: Use Windows system font (no copy needed)
# Just reference C:\Windows\Fonts\msyh.ttc in the script
The input is a Markdown string. Key structural elements to handle:
| Markdown Element | PDF Rendering |
|---|---|
# H1 | Large bold header |
## H2 | Medium bold header |
### H3 | Small bold header |
| Plain text | Body paragraph |
| ` | table |
**bold** | Bold text |
- list | Bullet point |
> quote | Indented italic |
--- | Horizontal rule |
`code` | Monospace |
🟢 / 🔴 | Color markers |
Create and run a Node.js script that uses pdfkit. Script location: skills/pdf-report-generator/scripts/generate_pdf.js
const PDFDocument = require('pdfkit');
const fs = require('fs');
// === CONFIG ===
const FONT_PATH = 'C:/Windows/Fonts/msyh.ttc'; // Chinese font
const FONT_BOLD_PATH = 'C:/Windows/Fonts/msyhbd.ttc'; // Bold variant (optional)
const PAGE_WIDTH = 595.28; // A4 width (points)
const PAGE_HEIGHT = 841.89; // A4 height (points)
const MARGIN = 72; // 1 inch margins
const HEADER_Y = 40;
const FOOTER_Y = PAGE_HEIGHT - 30;
const CONTENT_TOP = 72;
const CONTENT_BOTTOM = PAGE_HEIGHT - 72;
// === HELPERS ===
function addHeader(doc, text) {
doc.fontSize(8).font(FONT_PATH)
.text(text, MARGIN, HEADER_Y, { align: 'left' });
doc.moveTo(MARGIN, HEADER_Y + 14)
.lineTo(PAGE_WIDTH - MARGIN, HEADER_Y + 14)
.stroke('#cccccc');
}
function addFooter(doc, pageNum) {
doc.fontSize(8).font(FONT_PATH)
.text(`第 ${pageNum} 页`, MARGIN, FOOTER_Y, { align: 'center' });
}
function renderTable(doc, headers, rows, startY) {
// Column widths: equal distribution
const colCount = headers.length;
const colWidth = (PAGE_WIDTH - 2 * MARGIN) / colCount;
let y = startY;
// Header row
doc.fontSize(9).font(FONT_BOLD_PATH || FONT_PATH);
headers.forEach((h, i) => {
doc.text(h, MARGIN + i * colWidth + 2, y + 2, {
width: colWidth - 4, align: 'center'
});
});
y += 20;
// Draw header line
doc.moveTo(MARGIN, y).lineTo(PAGE_WIDTH - MARGIN, y).stroke();
// Data rows
doc.fontSize(9).font(FONT_PATH);
for (const row of rows) {
row.forEach((cell, i) => {
doc.text(String(cell), MARGIN + i * colWidth + 2, y + 2, {
width: colWidth - 4, align: 'center'
});
});
y += 18;
doc.moveTo(MARGIN, y).lineTo(PAGE_WIDTH - MARGIN, y)
.stroke('#eeeeee');
}
return y; // Return next Y position
}
// === MAIN ===
function generatePDF(markdownContent, outputPath, title) {
const doc = new PDFDocument({
size: 'A4',
margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN },
info: {
Title: title || 'Market Report',
Author: 'OpenClaw 虾哥',
Producer: 'OpenClaw pdf-report-generator',
}
});
const stream = fs.createWriteStream(outputPath);
doc.pipe(stream);
let pageNum = 1;
addHeader(doc, title || '市场报告');
addFooter(doc, pageNum);
// --- Parse and render markdown ---
const lines = markdownContent.split('\n');
let y = CONTENT_TOP;
for (const line of lines) {
// Check if we need a new page
if (y > CONTENT_BOTTOM - 30) {
doc.addPage();
pageNum++;
addHeader(doc, title || '市场报告');
addFooter(doc, pageNum);
y = CONTENT_TOP;
}
// H1
if (line.startsWith('# ')) {
doc.fontSize(18).font(FONT_BOLD_PATH || FONT_PATH)
.fillColor('#1a1a2e');
y = doc.text(line.slice(2), MARGIN, y + 16, {
continued: false
}).y + 6;
}
// H2
else if (line.startsWith('## ')) {
doc.fontSize(14).font(FONT_BOLD_PATH || FONT_PATH)
.fillColor('#16213e');
y = doc.text(line.slice(3), MARGIN, y + 12, {
continued: false
}).y + 4;
}
// H3
else if (line.startsWith('### ')) {
doc.fontSize(12).font(FONT_BOLD_PATH || FONT_PATH)
.fillColor('#0f3460');
y = doc.text(line.slice(4), MARGIN, y + 10, {
continued: false
}).y + 2;
}
// Horizontal rule
else if (line.startsWith('---')) {
doc.moveTo(MARGIN, y + 4)
.lineTo(PAGE_WIDTH - MARGIN, y + 4)
.stroke('#cccccc');
y += 12;
}
// Empty line
else if (line.trim() === '') {
y += 8;
}
// Regular text / bullet / table row
else {
doc.fontSize(10).font(FONT_PATH).fillColor('#333333');
y = doc.text(line, MARGIN, y + 2, {
width: PAGE_WIDTH - 2 * MARGIN,
lineGap: 2
}).y + 2;
}
doc.fillColor('#000000');
}
doc.end();
return new Promise((resolve, reject) => {
stream.on('finish', () => resolve(outputPath));
stream.on('error', reject);
});
}
// Export for use as module
module.exports = { generatePDF };
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
const mdFile = args[0];
const outFile = args[1] || 'report.pdf';
const title = args[2] || '市场报告';
const md = fs.readFileSync(mdFile, 'utf-8');
generatePDF(md, outFile, title).then(() => {
console.log(`PDF generated: ${outFile}`);
});
}
# From workspace root:
node skills/pdf-report-generator/scripts/generate_pdf.js input.md output.pdf "市场日报"
After daily-market-report produces a Markdown string, call this skill:
.tmp-report/latest.md)import subprocess
import tempfile
import os
def markdown_to_pdf(markdown_content, output_path, title="市场报告"):
"""Convert Markdown string to PDF file."""
# Write temp markdown
with tempfile.NamedTemporaryFile(
mode='w', suffix='.md', delete=False, encoding='utf-8'
) as f:
f.write(markdown_content)
md_path = f.name
try:
# Call Node.js generator
subprocess.run([
'node',
'skills/pdf-report-generator/scripts/generate_pdf.js',
md_path, output_path, title
], check=True, cwd=os.getenv('OPENCLAW_WORKSPACE', '.'))
return output_path
finally:
os.unlink(md_path)
Place Chinese TrueType font files here for portable use:
NotoSansSC-Regular.ttf — Main Chinese fontNotoSansSC-Bold.ttf — Bold variant (optional)NotoSansSC-Light.ttf — Light variant (optional)If no font file is in assets/, the script falls back to C:\Windows\Fonts\msyh.ttc.
generate_pdf.js — Main PDF generation script (see template above)