- name
- markdown-to-pdf
- description
- Converting Markdown to professional PDFs using Pandoc, WeasyPrint, ReportLab, and related tools
- metadata
- {"author":"cosmicstack-labs","version":"1.0.0","category":"pdf-generation","tags":["markdown","pdf","pandoc","weasyprint","reportlab","conversion"]}
# Markdown to PDF Conversion
Convert Markdown documents into polished, production-ready PDFs. This skill covers the major toolchains — Pandoc, WeasyPrint, ReportLab — plus CSS print styling, template systems, and batch conversion workflows.
---
## Toolchain Overview
| Tool | Approach | Best For | Output Quality |
|------|----------|----------|----------------|
| **Pandoc** | Markdown → PDF via LaTeX/Context/Wkhtmltopdf | Books, papers, reports | High |
| **WeasyPrint** | Markdown → HTML → PDF via CSS print | Web-like documents, branded PDFs | High |
| **ReportLab** | Programmatic PDF generation via Python | Dynamic, data-driven PDFs | Very High |
| **md-to-pdf** | Node.js CLI for quick markdown → PDF | Simple documents, quick output | Medium |
| **Puppeteer/Playwright** | Markdown → HTML → Headless browser → PDF | Pixel-perfect web-to-PDF | Very High |
---
## 1. Pandoc Workflows
Pandoc is the Swiss Army knife of document conversion. It reads Markdown and outputs PDF via an intermediate engine (default: LaTeX).
### Basic Conversion
```bash
# Simple markdown to PDF (uses pdflatex)
pandoc input.md -o output.pdf
# With metadata file
pandoc input.md --metadata-file=metadata.yaml -o output.pdf
# Specify output engine
pandoc input.md --pdf-engine=xelatex -o output.pdf
# Using wkhtmltopdf instead of LaTeX
pandoc input.md --pdf-engine=wkhtmltopdf -o output.pdf
```
### Advanced Pandoc with Templates
```bash
# Custom LaTeX template
pandoc input.md \
--template=custom-template.tex \
--pdf-engine=xelatex \
--toc \
--number-sections \
-o output.pdf
# With bibliography
pandoc input.md \
--bibliography=references.bib \
--csl=ieee.csl \
--citeproc \
-o output.pdf
# Custom font and margins
pandoc input.md \
-V geometry:"top=2cm, bottom=2cm, left=2.5cm, right=2.5cm" \
-V mainfont="Times New Roman" \
-V fontsize=12pt \
-o output.pdf
```
### Metadata YAML for Pandoc
```yaml
# metadata.yaml
---
title: "Technical Report on Distributed Systems"
author:
- "Jane Doe"
- "John Smith"
date: "2025-01-15"
subtitle: "Performance Analysis of Event-Driven Architectures"
abstract: |
This report analyzes the performance characteristics of event-driven
architectures in distributed systems, comparing Apache Kafka, RabbitMQ,
and AWS SQS across latency, throughput, and fault tolerance metrics.
keywords: [distributed-systems, event-driven, kafka, rabbitmq, performance]
lang: en-US
geometry:
- top=25mm
- bottom=25mm
- left=30mm
- right=25mm
fontsize: 11pt
mainfont: "DejaVu Serif"
monofont: "DejaVu Sans Mono"
toc: true
toc-depth: 3
numbersections: true
linestretch: 1.5
---
```
### Pandoc Filter: Custom Transformations
```python
#!/usr/bin/env python3
"""Pandoc filter to add custom styling to code blocks."""
import pandocfilters as pf
def code_blocks(key, value, format, meta):
"""Wrap code blocks in a styled container."""
if key == 'CodeBlock':
[[ident, classes, keyvals], code] = value
if 'python' in classes:
# Add a custom div wrapper for Python code
return pf.Div(
([ident, ['python-block'], keyvals],
[pf.CodeBlock([ident, classes, keyvals], code)])
)
if __name__ == '__main__':
pf.toJSONFilter(code_blocks)
```
```bash
# Use the filter
pandoc input.md --filter=./code_styler.py -o output.pdf
```
---
## 2. WeasyPrint: HTML/CSS to PDF
WeasyPrint renders HTML with CSS print styles into PDFs. It's ideal for documents that need pixel-perfect branding, colors, and web-like layouts.
### Basic WeasyPrint Workflow
```python
"""Convert HTML to PDF using WeasyPrint."""
from weasyprint import HTML
# Basic conversion
HTML('document.html').write_pdf('output.pdf')
# From a URL
HTML('https://example.com/report').write_pdf('webpage.pdf')
# From a string
html_content = """
<html>
<body>
<h1>Hello World</h1>
<p>This is a PDF generated from an HTML string.</p>
</body>
</html>
"""
HTML(string=html_content).write_pdf('from_string.pdf')
```
### Markdown → HTML → PDF Pipeline
```python
"""Full pipeline: Markdown to HTML to PDF with CSS styling."""
import markdown
from weasyprint import HTML
def markdown_to_pdf(md_path, css_path, output_path):
"""Convert markdown to PDF through HTML intermediate."""
with open(md_path, 'r') as f:
md_content = f.read()
# Convert markdown to HTML
html_content = markdown.markdown(
md_content,
extensions=['tables', 'fenced_code', 'codehilite',
'toc', 'sane_lists', 'attr_list']
)
# Wrap in full HTML document
full_html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="{css_path}">
</head>
<body>
{html_content}
</body>
</html>
"""
# Generate PDF
HTML(string=full_html).write_pdf(output_path)
print(f"PDF generated: {output_path}")
# Usage
markdown_to_pdf('report.md', 'print.css', 'report.pdf')
```
### CSS Print Stylesheet Template
```css
/* print.css — Professional print stylesheet for PDF generation */
/* Page setup */
@page {
size: A4;
margin: 2.5cm 2cm 2.5cm 2cm;
@top-center {
content: element(pageHeader);
font-size: 9pt;
color: #666;
}
@bottom-center {
content: counter(page) " / " counter(pages);
font-size: 9pt;
color: #666;
}
@bottom-left {
content: "Confidential";
font-size: 8pt;
color: #999;
font-style: italic;
}
}
/* First page — no header, different margins */
@page:first {
margin-top: 4cm;
@top-center {
content: none;
}
}
/* Section start — reset page counter */
@page chapter {
@top-center {
content: "Chapter " counter(chapter);
}
}
/* Base typography */
body {
font-family: "DejaVu Serif", Georgia, "Times New Roman", serif;
font-size: 11pt;
line-height: 1.6;
color: #1a1a1a;
counter-reset: h2 h3 figure table;
}
/* Heading styles */
h1 {
font-size: 24pt;
font-weight: bold;
color: #1a1a1a;
page-break-before: always;
page-break-after: avoid;
margin-top: 2cm;
border-bottom: 2px solid #333;
padding-bottom: 8pt;
}
h1:first-of-type {
page-break-before: avoid;
}
h2 {
font-size: 18pt;
font-weight: bold;
color: #333;
page-break-after: avoid;
margin-top: 1.5cm;
counter-increment: h2;
}
h2::before {
content: counter(h2) ". ";
}
h3 {
font-size: 14pt;
font-weight: bold;
color: #555;
page-break-after: avoid;
margin-top: 1cm;
counter-increment: h3;
}
h3::before {
content: counter(h2) "." counter(h3) " ";
}
/* Paragraphs */
p {
text-align: justify;
orphans: 3;
widows: 3;
}
/* Code blocks */
pre {
font-family: "DejaVu Sans Mono", "Courier New", monospace;
font-size: 9pt;
background: #f5f5f5;
border: 1px solid #ddd;
border-left: 3px solid #007acc;
padding: 12px;
page-break-inside: avoid;
white-space: pre-wrap;
word-wrap: break-word;
}
code {
font-family: "DejaVu Sans Mono", "Courier New", monospace;
font-size: 9pt;
background: #f0f0f0;
padding: 1px 4px;
border-radius: 2px;
}
pre code {
background: none;
padding: 0;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
font-size: 10pt;
page-break-inside: auto;
}
thead {
display: table-header-group;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
th {
background: #2c3e50;
color: white;
font-weight: bold;
padding: 8px 12px;
text-align: left;
}
td {
padding: 6px 12px;
border-bottom: 1px solid #ddd;
}
tr:nth-child(even) {
background: #f9f9f9;
}
/* Images */
img {
max-width: 100%;
height: auto;
page-break-inside: avoid;
}
figure {
margin: 1em 0;
text-align: center;
page-break-inside: avoid;
}
figcaption {
font-size: 10pt;
font-style: italic;
color: #666;
margin-top: 4px;
}
/* Links */
a {
color: #0066cc;
text-decoration: none;
}
/* Lists */
ul, ol {
margin: 0.5em 0;
padding-left: 2em;
}
li {
margin: 0.3em 0;
}
/* Blockquotes */
blockquote {
border-left: 4px solid #2c3e50;
margin: 1em 0;
padding: 0.5em 1em;
background: #f9f9f9;
font-style: italic;
}
/* Cover page */
.cover-page {
page-break-after: always;
text-align: center;
padding-top: 6cm;
}
.cover-page h1 {
font-size: 32pt;
border: none;
margin-bottom: 0.5cm;
}
.cover-page .subtitle {
font-size: 18pt;
color: #666;
margin-bottom: 2cm;
}
.cover-page .author {
font-size: 14pt;
color: #333;
}
.cover-page .date {
font-size: 12pt;
color: #999;
margin-top: 1cm;
}
/* Table of contents */
.toc {
page-break-after: always;
}
.toc h2 {
border: none;
font-size: 20pt;
}
.toc ul {
list-style: none;
padding: 0;
}
.toc li {
padding: 4px 0;
border-bottom: 1px dotted #ccc;
}
.toc a {
color: #333;
text-decoration: none;
}
.toc .toc-h2 {
font-weight: bold;
padding-left: 0;
}
.toc .toc-h3 {
padding-left: 2em;
font-size: 10pt;
}
```
---
## 3. ReportLab: Programmatic PDF Generation
ReportLab gives you complete control over PDF layout programmatically from Python.
### Basic ReportLab Document
```python
"""Create a basic PDF with ReportLab."""
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm, mm
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
Image, PageBreak, ListFlowable, ListItem
)
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
def create_report(output_path):
"""Generate a professional report PDF."""
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
rightMargin=2*cm,
leftMargin=2*cm,
topMargin=2*cm,
bottomMargin=2*cm,
title="Annual Report 2025",
author="Data Analytics Team",
)
styles = getSampleStyleSheet()
story = []
# Title page
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Title'],
fontSize=28,
leading=34,
spaceAfter=20,
alignment=TA_CENTER,
textColor=colors.HexColor('#1a1a2e'),
)
story.append(Spacer(1, 5*cm))
story.append(Paragraph("Annual Report 2025", title_style))
Auf GitHub ansehen