simpdf 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simpdf-0.1.0/PKG-INFO +211 -0
- simpdf-0.1.0/README.md +191 -0
- simpdf-0.1.0/pyproject.toml +45 -0
- simpdf-0.1.0/setup.cfg +4 -0
- simpdf-0.1.0/simpdf/__init__.py +11 -0
- simpdf-0.1.0/simpdf/__main__.py +5 -0
- simpdf-0.1.0/simpdf/cli.py +58 -0
- simpdf-0.1.0/simpdf/fonts.py +122 -0
- simpdf-0.1.0/simpdf/markdown.py +90 -0
- simpdf-0.1.0/simpdf/options.py +124 -0
- simpdf-0.1.0/simpdf/pdfgen.py +39 -0
- simpdf-0.1.0/simpdf/renderer.py +653 -0
- simpdf-0.1.0/simpdf.egg-info/PKG-INFO +211 -0
- simpdf-0.1.0/simpdf.egg-info/SOURCES.txt +20 -0
- simpdf-0.1.0/simpdf.egg-info/dependency_links.txt +1 -0
- simpdf-0.1.0/simpdf.egg-info/entry_points.txt +2 -0
- simpdf-0.1.0/simpdf.egg-info/requires.txt +5 -0
- simpdf-0.1.0/simpdf.egg-info/top_level.txt +1 -0
- simpdf-0.1.0/tests/test_cli.py +57 -0
- simpdf-0.1.0/tests/test_fonts.py +97 -0
- simpdf-0.1.0/tests/test_markdown.py +29 -0
- simpdf-0.1.0/tests/test_renderer.py +87 -0
simpdf-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simpdf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Markdown-to-PDF rendering with FPDF2 and configurable TTF fonts.
|
|
5
|
+
Author: Dmitry
|
|
6
|
+
Keywords: markdown,pdf,fpdf,cyrillic
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: fpdf2<3,>=2.8.0
|
|
17
|
+
Requires-Dist: markdown-it-py<4,>=3.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest<9,>=8.0.0; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# simpdf
|
|
22
|
+
|
|
23
|
+
`simpdf` is a small Python library for rendering Markdown into PDF with `fpdf2`, while keeping font handling explicit so Cyrillic and other non-Latin text work reliably with external TTF fonts.
|
|
24
|
+
|
|
25
|
+
The library is centered on a `MarkdownPdfRenderer` class. You give it a directory with TTF files, describe the font face to register, and then render Markdown into PDF bytes or directly into a file.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Uses `fpdf2` as the PDF backend
|
|
30
|
+
- Supports external TTF fonts and Cyrillic-friendly fonts such as DejaVu Sans
|
|
31
|
+
- Provides a helper to download DejaVu Sans fonts into a target directory
|
|
32
|
+
- Class-first API with optional convenience helpers
|
|
33
|
+
- Minimal CLI for rendering Markdown and downloading DejaVu fonts
|
|
34
|
+
- Supports these Markdown elements in v1:
|
|
35
|
+
- headings
|
|
36
|
+
- paragraphs
|
|
37
|
+
- bold and italic text
|
|
38
|
+
- inline code
|
|
39
|
+
- ordered and unordered lists
|
|
40
|
+
- tables
|
|
41
|
+
- blockquotes
|
|
42
|
+
- fenced code blocks
|
|
43
|
+
- thematic breaks
|
|
44
|
+
- clickable links
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install simpdf
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For development and tests:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e .[dev]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from simpdf import FontFace, MarkdownPdfRenderer
|
|
62
|
+
|
|
63
|
+
renderer = MarkdownPdfRenderer(
|
|
64
|
+
font_directory="fonts",
|
|
65
|
+
font_face=FontFace.dejavu_sans(),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
markdown_text = """
|
|
69
|
+
# Example
|
|
70
|
+
|
|
71
|
+
Привет, мир.
|
|
72
|
+
|
|
73
|
+
- one
|
|
74
|
+
- two
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
pdf_bytes = renderer.render_to_bytes(markdown_text)
|
|
78
|
+
|
|
79
|
+
with open("output.pdf", "wb") as handle:
|
|
80
|
+
handle.write(pdf_bytes)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Downloading DejaVu Fonts
|
|
84
|
+
|
|
85
|
+
The library does not bundle fonts inside the wheel. Use the download helper to populate your own font directory.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from simpdf import download_dejavu_fonts
|
|
89
|
+
|
|
90
|
+
downloaded = download_dejavu_fonts("fonts")
|
|
91
|
+
print([path.name for path in downloaded])
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The helper downloads the DejaVu font files from this public GitHub repository layout:
|
|
95
|
+
|
|
96
|
+
- `https://github.com/shwars/simpdf/raw/refs/heads/main/fonts/DejaVuSans.ttf`
|
|
97
|
+
- other font files use the same URL pattern
|
|
98
|
+
|
|
99
|
+
## Using Custom Fonts
|
|
100
|
+
|
|
101
|
+
You can use any TTF family as long as you provide at least a regular face. Bold, italic, and bold-italic are optional; if omitted, the regular face is reused.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from simpdf import FontFace, MarkdownPdfRenderer
|
|
105
|
+
|
|
106
|
+
renderer = MarkdownPdfRenderer(
|
|
107
|
+
font_directory="my-fonts",
|
|
108
|
+
font_face=FontFace(
|
|
109
|
+
family="NotoSansCustom",
|
|
110
|
+
regular="NotoSans-Regular.ttf",
|
|
111
|
+
bold="NotoSans-Bold.ttf",
|
|
112
|
+
italic="NotoSans-Italic.ttf",
|
|
113
|
+
bold_italic="NotoSans-BoldItalic.ttf",
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Font file values may be file names relative to `font_directory` or absolute paths.
|
|
119
|
+
|
|
120
|
+
## Formatting Options
|
|
121
|
+
|
|
122
|
+
Formatting is configured with a plain nested dictionary. Any omitted value falls back to the library defaults.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from simpdf import FontFace, MarkdownPdfRenderer
|
|
128
|
+
|
|
129
|
+
renderer = MarkdownPdfRenderer(
|
|
130
|
+
font_directory="fonts",
|
|
131
|
+
font_face=FontFace.dejavu_sans(),
|
|
132
|
+
formatting_options={
|
|
133
|
+
"text": {"font_size": 11},
|
|
134
|
+
"headings": {
|
|
135
|
+
"sizes": {1: 26, 2: 20, 3: 16},
|
|
136
|
+
},
|
|
137
|
+
"lists": {"indent": 9},
|
|
138
|
+
"table": {"heading_font_size": 13},
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Supported option groups:
|
|
144
|
+
|
|
145
|
+
- `page`: page size, orientation, and margins
|
|
146
|
+
- `text`: base font size, line height multiplier, text color
|
|
147
|
+
- `headings`: per-level sizes and spacing
|
|
148
|
+
- `paragraph`: paragraph spacing
|
|
149
|
+
- `lists`: indent, bullet symbol, list spacing
|
|
150
|
+
- `blockquote`: indent, bar styling, text color
|
|
151
|
+
- `table`: font sizes, padding, minimum column width, spacing
|
|
152
|
+
- `code_block`: font size, padding, colors, spacing
|
|
153
|
+
- `inline_code`: inline code text color
|
|
154
|
+
- `links`: link color and underline toggle
|
|
155
|
+
- `thematic_break`: rule color, width, spacing
|
|
156
|
+
|
|
157
|
+
## Convenience Helpers
|
|
158
|
+
|
|
159
|
+
If you prefer a functional call site, `simpdf` also exports:
|
|
160
|
+
|
|
161
|
+
- `render_markdown_to_pdf_bytes(...)`
|
|
162
|
+
- `render_markdown_to_pdf_file(...)`
|
|
163
|
+
|
|
164
|
+
These helpers internally construct `MarkdownPdfRenderer`.
|
|
165
|
+
|
|
166
|
+
## CLI Usage
|
|
167
|
+
|
|
168
|
+
Render Markdown into PDF:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
simpdf render input.md output.pdf \
|
|
172
|
+
--fonts-dir ./fonts \
|
|
173
|
+
--family-name DejaVuSans \
|
|
174
|
+
--font-regular DejaVuSans.ttf \
|
|
175
|
+
--font-bold DejaVuSans-Bold.ttf \
|
|
176
|
+
--font-italic DejaVuSans-Oblique.ttf \
|
|
177
|
+
--font-bold-italic DejaVuSans-BoldOblique.ttf
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Download DejaVu fonts:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
simpdf download-dejavu ./fonts
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If you want custom formatting from the CLI, pass a JSON file with `--options-file`.
|
|
187
|
+
|
|
188
|
+
## Examples
|
|
189
|
+
|
|
190
|
+
See [`examples/basic_render.py`](/D:/GIT/simpdf/examples/basic_render.py), [`examples/custom_font_and_style.py`](/D:/GIT/simpdf/examples/custom_font_and_style.py), and [`examples/rich_markdown.py`](/D:/GIT/simpdf/examples/rich_markdown.py).
|
|
191
|
+
|
|
192
|
+
## Tests
|
|
193
|
+
|
|
194
|
+
The repo now contains a pytest suite that covers:
|
|
195
|
+
|
|
196
|
+
- font config validation
|
|
197
|
+
- DejaVu download helper behavior
|
|
198
|
+
- markdown token flattening and table extraction
|
|
199
|
+
- PDF rendering with Cyrillic content
|
|
200
|
+
- formatting overrides
|
|
201
|
+
- CLI render and download flows
|
|
202
|
+
|
|
203
|
+
Run tests with:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pytest
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Compatibility Note
|
|
210
|
+
|
|
211
|
+
For older code that imported `simpdf.pdfgen`, a small compatibility wrapper is still present. The recommended API is the class-based renderer from `simpdf`.
|
simpdf-0.1.0/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# simpdf
|
|
2
|
+
|
|
3
|
+
`simpdf` is a small Python library for rendering Markdown into PDF with `fpdf2`, while keeping font handling explicit so Cyrillic and other non-Latin text work reliably with external TTF fonts.
|
|
4
|
+
|
|
5
|
+
The library is centered on a `MarkdownPdfRenderer` class. You give it a directory with TTF files, describe the font face to register, and then render Markdown into PDF bytes or directly into a file.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Uses `fpdf2` as the PDF backend
|
|
10
|
+
- Supports external TTF fonts and Cyrillic-friendly fonts such as DejaVu Sans
|
|
11
|
+
- Provides a helper to download DejaVu Sans fonts into a target directory
|
|
12
|
+
- Class-first API with optional convenience helpers
|
|
13
|
+
- Minimal CLI for rendering Markdown and downloading DejaVu fonts
|
|
14
|
+
- Supports these Markdown elements in v1:
|
|
15
|
+
- headings
|
|
16
|
+
- paragraphs
|
|
17
|
+
- bold and italic text
|
|
18
|
+
- inline code
|
|
19
|
+
- ordered and unordered lists
|
|
20
|
+
- tables
|
|
21
|
+
- blockquotes
|
|
22
|
+
- fenced code blocks
|
|
23
|
+
- thematic breaks
|
|
24
|
+
- clickable links
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install simpdf
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For development and tests:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e .[dev]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from simpdf import FontFace, MarkdownPdfRenderer
|
|
42
|
+
|
|
43
|
+
renderer = MarkdownPdfRenderer(
|
|
44
|
+
font_directory="fonts",
|
|
45
|
+
font_face=FontFace.dejavu_sans(),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
markdown_text = """
|
|
49
|
+
# Example
|
|
50
|
+
|
|
51
|
+
Привет, мир.
|
|
52
|
+
|
|
53
|
+
- one
|
|
54
|
+
- two
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
pdf_bytes = renderer.render_to_bytes(markdown_text)
|
|
58
|
+
|
|
59
|
+
with open("output.pdf", "wb") as handle:
|
|
60
|
+
handle.write(pdf_bytes)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Downloading DejaVu Fonts
|
|
64
|
+
|
|
65
|
+
The library does not bundle fonts inside the wheel. Use the download helper to populate your own font directory.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from simpdf import download_dejavu_fonts
|
|
69
|
+
|
|
70
|
+
downloaded = download_dejavu_fonts("fonts")
|
|
71
|
+
print([path.name for path in downloaded])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The helper downloads the DejaVu font files from this public GitHub repository layout:
|
|
75
|
+
|
|
76
|
+
- `https://github.com/shwars/simpdf/raw/refs/heads/main/fonts/DejaVuSans.ttf`
|
|
77
|
+
- other font files use the same URL pattern
|
|
78
|
+
|
|
79
|
+
## Using Custom Fonts
|
|
80
|
+
|
|
81
|
+
You can use any TTF family as long as you provide at least a regular face. Bold, italic, and bold-italic are optional; if omitted, the regular face is reused.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from simpdf import FontFace, MarkdownPdfRenderer
|
|
85
|
+
|
|
86
|
+
renderer = MarkdownPdfRenderer(
|
|
87
|
+
font_directory="my-fonts",
|
|
88
|
+
font_face=FontFace(
|
|
89
|
+
family="NotoSansCustom",
|
|
90
|
+
regular="NotoSans-Regular.ttf",
|
|
91
|
+
bold="NotoSans-Bold.ttf",
|
|
92
|
+
italic="NotoSans-Italic.ttf",
|
|
93
|
+
bold_italic="NotoSans-BoldItalic.ttf",
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Font file values may be file names relative to `font_directory` or absolute paths.
|
|
99
|
+
|
|
100
|
+
## Formatting Options
|
|
101
|
+
|
|
102
|
+
Formatting is configured with a plain nested dictionary. Any omitted value falls back to the library defaults.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from simpdf import FontFace, MarkdownPdfRenderer
|
|
108
|
+
|
|
109
|
+
renderer = MarkdownPdfRenderer(
|
|
110
|
+
font_directory="fonts",
|
|
111
|
+
font_face=FontFace.dejavu_sans(),
|
|
112
|
+
formatting_options={
|
|
113
|
+
"text": {"font_size": 11},
|
|
114
|
+
"headings": {
|
|
115
|
+
"sizes": {1: 26, 2: 20, 3: 16},
|
|
116
|
+
},
|
|
117
|
+
"lists": {"indent": 9},
|
|
118
|
+
"table": {"heading_font_size": 13},
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Supported option groups:
|
|
124
|
+
|
|
125
|
+
- `page`: page size, orientation, and margins
|
|
126
|
+
- `text`: base font size, line height multiplier, text color
|
|
127
|
+
- `headings`: per-level sizes and spacing
|
|
128
|
+
- `paragraph`: paragraph spacing
|
|
129
|
+
- `lists`: indent, bullet symbol, list spacing
|
|
130
|
+
- `blockquote`: indent, bar styling, text color
|
|
131
|
+
- `table`: font sizes, padding, minimum column width, spacing
|
|
132
|
+
- `code_block`: font size, padding, colors, spacing
|
|
133
|
+
- `inline_code`: inline code text color
|
|
134
|
+
- `links`: link color and underline toggle
|
|
135
|
+
- `thematic_break`: rule color, width, spacing
|
|
136
|
+
|
|
137
|
+
## Convenience Helpers
|
|
138
|
+
|
|
139
|
+
If you prefer a functional call site, `simpdf` also exports:
|
|
140
|
+
|
|
141
|
+
- `render_markdown_to_pdf_bytes(...)`
|
|
142
|
+
- `render_markdown_to_pdf_file(...)`
|
|
143
|
+
|
|
144
|
+
These helpers internally construct `MarkdownPdfRenderer`.
|
|
145
|
+
|
|
146
|
+
## CLI Usage
|
|
147
|
+
|
|
148
|
+
Render Markdown into PDF:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
simpdf render input.md output.pdf \
|
|
152
|
+
--fonts-dir ./fonts \
|
|
153
|
+
--family-name DejaVuSans \
|
|
154
|
+
--font-regular DejaVuSans.ttf \
|
|
155
|
+
--font-bold DejaVuSans-Bold.ttf \
|
|
156
|
+
--font-italic DejaVuSans-Oblique.ttf \
|
|
157
|
+
--font-bold-italic DejaVuSans-BoldOblique.ttf
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Download DejaVu fonts:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
simpdf download-dejavu ./fonts
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
If you want custom formatting from the CLI, pass a JSON file with `--options-file`.
|
|
167
|
+
|
|
168
|
+
## Examples
|
|
169
|
+
|
|
170
|
+
See [`examples/basic_render.py`](/D:/GIT/simpdf/examples/basic_render.py), [`examples/custom_font_and_style.py`](/D:/GIT/simpdf/examples/custom_font_and_style.py), and [`examples/rich_markdown.py`](/D:/GIT/simpdf/examples/rich_markdown.py).
|
|
171
|
+
|
|
172
|
+
## Tests
|
|
173
|
+
|
|
174
|
+
The repo now contains a pytest suite that covers:
|
|
175
|
+
|
|
176
|
+
- font config validation
|
|
177
|
+
- DejaVu download helper behavior
|
|
178
|
+
- markdown token flattening and table extraction
|
|
179
|
+
- PDF rendering with Cyrillic content
|
|
180
|
+
- formatting overrides
|
|
181
|
+
- CLI render and download flows
|
|
182
|
+
|
|
183
|
+
Run tests with:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pytest
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Compatibility Note
|
|
190
|
+
|
|
191
|
+
For older code that imported `simpdf.pdfgen`, a small compatibility wrapper is still present. The recommended API is the class-based renderer from `simpdf`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simpdf"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Markdown-to-PDF rendering with FPDF2 and configurable TTF fonts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Dmitry"}
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"fpdf2>=2.8.0,<3",
|
|
16
|
+
"markdown-it-py>=3.0.0,<4",
|
|
17
|
+
]
|
|
18
|
+
keywords = ["markdown", "pdf", "fpdf", "cyrillic"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0.0,<9",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
simpdf = "simpdf.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
include-package-data = false
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["simpdf*"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
addopts = "-q"
|
simpdf-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .fonts import DEJAVU_FONT_FILES, FontFace, download_dejavu_fonts
|
|
2
|
+
from .renderer import MarkdownPdfRenderer, render_markdown_to_pdf_bytes, render_markdown_to_pdf_file
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"DEJAVU_FONT_FILES",
|
|
6
|
+
"FontFace",
|
|
7
|
+
"MarkdownPdfRenderer",
|
|
8
|
+
"download_dejavu_fonts",
|
|
9
|
+
"render_markdown_to_pdf_bytes",
|
|
10
|
+
"render_markdown_to_pdf_file",
|
|
11
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .fonts import FontFace, download_dejavu_fonts
|
|
8
|
+
from .renderer import MarkdownPdfRenderer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(prog="simpdf", description="Render Markdown into PDF with configurable TTF fonts.")
|
|
13
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
|
|
15
|
+
render_parser = subparsers.add_parser("render", help="Render a Markdown file into PDF.")
|
|
16
|
+
render_parser.add_argument("input_markdown", type=Path)
|
|
17
|
+
render_parser.add_argument("output_pdf", type=Path)
|
|
18
|
+
render_parser.add_argument("--fonts-dir", type=Path, required=True)
|
|
19
|
+
render_parser.add_argument("--family-name", default="DejaVuSans")
|
|
20
|
+
render_parser.add_argument("--font-regular", required=True)
|
|
21
|
+
render_parser.add_argument("--font-bold")
|
|
22
|
+
render_parser.add_argument("--font-italic")
|
|
23
|
+
render_parser.add_argument("--font-bold-italic")
|
|
24
|
+
render_parser.add_argument("--options-file", type=Path, help="Path to a JSON file with formatting options.")
|
|
25
|
+
|
|
26
|
+
download_parser = subparsers.add_parser("download-dejavu", help="Download the DejaVu Sans font files.")
|
|
27
|
+
download_parser.add_argument("target_dir", type=Path)
|
|
28
|
+
download_parser.add_argument("--overwrite", action="store_true")
|
|
29
|
+
|
|
30
|
+
return parser
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main(argv: list[str] | None = None) -> int:
|
|
34
|
+
parser = build_parser()
|
|
35
|
+
args = parser.parse_args(argv)
|
|
36
|
+
|
|
37
|
+
if args.command == "download-dejavu":
|
|
38
|
+
download_dejavu_fonts(args.target_dir, overwrite=args.overwrite)
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
options = {}
|
|
42
|
+
if args.options_file:
|
|
43
|
+
options = json.loads(args.options_file.read_text(encoding="utf-8"))
|
|
44
|
+
|
|
45
|
+
renderer = MarkdownPdfRenderer(
|
|
46
|
+
font_directory=args.fonts_dir,
|
|
47
|
+
font_face=FontFace(
|
|
48
|
+
family=args.family_name,
|
|
49
|
+
regular=args.font_regular,
|
|
50
|
+
bold=args.font_bold,
|
|
51
|
+
italic=args.font_italic,
|
|
52
|
+
bold_italic=args.font_bold_italic,
|
|
53
|
+
),
|
|
54
|
+
formatting_options=options,
|
|
55
|
+
)
|
|
56
|
+
markdown_text = args.input_markdown.read_text(encoding="utf-8")
|
|
57
|
+
renderer.render_to_file(markdown_text, args.output_pdf)
|
|
58
|
+
return 0
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Mapping
|
|
6
|
+
from urllib.request import urlopen
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEJAVU_BASE_URL = "https://github.com/shwars/simpdf/raw/refs/heads/main/fonts"
|
|
10
|
+
DEJAVU_FONT_FILES = (
|
|
11
|
+
"DejaVuSans.ttf",
|
|
12
|
+
"DejaVuSans-Bold.ttf",
|
|
13
|
+
"DejaVuSans-Oblique.ttf",
|
|
14
|
+
"DejaVuSans-BoldOblique.ttf",
|
|
15
|
+
"DejaVuSansCondensed.ttf",
|
|
16
|
+
"DejaVuSansCondensed-Bold.ttf",
|
|
17
|
+
"DejaVuSansCondensed-Oblique.ttf",
|
|
18
|
+
"DejaVuSansCondensed-BoldOblique.ttf",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class FontFace:
|
|
24
|
+
family: str
|
|
25
|
+
regular: str
|
|
26
|
+
bold: str | None = None
|
|
27
|
+
italic: str | None = None
|
|
28
|
+
bold_italic: str | None = None
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def dejavu_sans(cls) -> "FontFace":
|
|
32
|
+
return cls(
|
|
33
|
+
family="DejaVuSans",
|
|
34
|
+
regular="DejaVuSans.ttf",
|
|
35
|
+
bold="DejaVuSans-Bold.ttf",
|
|
36
|
+
italic="DejaVuSans-Oblique.ttf",
|
|
37
|
+
bold_italic="DejaVuSans-BoldOblique.ttf",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def coerce_font_face(font_face: FontFace | Mapping[str, str]) -> FontFace:
|
|
42
|
+
if isinstance(font_face, FontFace):
|
|
43
|
+
return font_face
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
family = font_face["family"]
|
|
47
|
+
regular = font_face["regular"]
|
|
48
|
+
except KeyError as exc:
|
|
49
|
+
raise ValueError(f"Missing required font face field: {exc.args[0]}") from exc
|
|
50
|
+
|
|
51
|
+
return FontFace(
|
|
52
|
+
family=family,
|
|
53
|
+
regular=regular,
|
|
54
|
+
bold=font_face.get("bold"),
|
|
55
|
+
italic=font_face.get("italic"),
|
|
56
|
+
bold_italic=font_face.get("bold_italic"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_font_paths(font_directory: str | Path, font_face: FontFace | Mapping[str, str]) -> dict[str, Path]:
|
|
61
|
+
directory = Path(font_directory)
|
|
62
|
+
face = coerce_font_face(font_face)
|
|
63
|
+
if not directory.exists():
|
|
64
|
+
raise FileNotFoundError(f"Font directory does not exist: {directory}")
|
|
65
|
+
|
|
66
|
+
regular = _resolve_path(directory, face.regular)
|
|
67
|
+
bold = _resolve_path(directory, face.bold) if face.bold else regular
|
|
68
|
+
italic = _resolve_path(directory, face.italic) if face.italic else regular
|
|
69
|
+
bold_italic = _resolve_path(directory, face.bold_italic) if face.bold_italic else bold
|
|
70
|
+
|
|
71
|
+
missing = [path for path in (regular, bold, italic, bold_italic) if not path.exists()]
|
|
72
|
+
if missing:
|
|
73
|
+
message = ", ".join(str(path) for path in missing)
|
|
74
|
+
raise FileNotFoundError(f"Missing required font files: {message}")
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"regular": regular,
|
|
78
|
+
"bold": bold,
|
|
79
|
+
"italic": italic,
|
|
80
|
+
"bold_italic": bold_italic,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def register_font_family(pdf, font_directory: str | Path, font_face: FontFace | Mapping[str, str]) -> str:
|
|
85
|
+
face = coerce_font_face(font_face)
|
|
86
|
+
resolved = resolve_font_paths(font_directory, face)
|
|
87
|
+
pdf.add_font(face.family, style="", fname=str(resolved["regular"]))
|
|
88
|
+
pdf.add_font(face.family, style="B", fname=str(resolved["bold"]))
|
|
89
|
+
pdf.add_font(face.family, style="I", fname=str(resolved["italic"]))
|
|
90
|
+
pdf.add_font(face.family, style="BI", fname=str(resolved["bold_italic"]))
|
|
91
|
+
return face.family
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def download_dejavu_fonts(
|
|
95
|
+
target_dir: str | Path,
|
|
96
|
+
*,
|
|
97
|
+
overwrite: bool = False,
|
|
98
|
+
timeout: int = 30,
|
|
99
|
+
) -> list[Path]:
|
|
100
|
+
target = Path(target_dir)
|
|
101
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
downloaded: list[Path] = []
|
|
103
|
+
|
|
104
|
+
for filename in DEJAVU_FONT_FILES:
|
|
105
|
+
destination = target / filename
|
|
106
|
+
if destination.exists() and not overwrite:
|
|
107
|
+
downloaded.append(destination)
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
url = f"{DEJAVU_BASE_URL}/{filename}"
|
|
111
|
+
with urlopen(url, timeout=timeout) as response:
|
|
112
|
+
destination.write_bytes(response.read())
|
|
113
|
+
downloaded.append(destination)
|
|
114
|
+
|
|
115
|
+
return downloaded
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _resolve_path(base_dir: Path, value: str) -> Path:
|
|
119
|
+
path = Path(value)
|
|
120
|
+
if path.is_absolute():
|
|
121
|
+
return path
|
|
122
|
+
return base_dir / path
|