md2pdf.py: Turning Markdown Docs Into Premium PDF Add-Ons With Reportlab
I sold a 40-page template pack last spring and the buyer emailed me one line: "the PDF looks like a printout of a text file." He was right. I had written the whole thing in Markdown, run it through a converter I found in
I sold a 40-page template pack last spring and the buyer emailed me one line: "the PDF looks like a printout of a text file." He was right. I had written the whole thing in Markdown, run it through a converter I found in a stack overflow answer, and shipped whatever came out. Headings were the same size as body text. Tables collapsed into runs of pipes. The cover was a screenshot. That single refund cost me more in confidence than in dollars, and it pushed me to write md2pdf.py properly with reportlab instead of hoping a generic converter would do.
Here is what I learned building it, and why it now sits in the middle of my pipeline.
Why Markdown is the right source format
Every product I sell has the same shape: a guide, a license page, a changelog, sometimes a usage tutorial. Writing those in a word processor means formatting by hand every time. Writing them in Markdown means the content is plain text I can diff, version, and regenerate. The PDF becomes an output, not a document I maintain.
That matters when you update. I fixed a license clause in March and regenerated three product PDFs in under a minute. If the formatting lived in the document, I would have edited three files by hand and missed something in one of them.
What reportlab actually gives you
reportlab is the only PDF dependency in my whole pipeline, alongside Pillow for covers. Two packages total. It is not glamorous, but it handles the four things a product PDF needs:
- Headings with real hierarchy, styled once in a stylesheet
- Tables that keep their columns, which is where the cheap converters die
- Lists, both ordered and bulleted, nested properly
- Clickable links, which matters because my guides link back to my store and to asset pages
The command is one line:
python md2pdf.py content.md product.pdf "My Product Title"
The title argument lands on a cover page. That is the difference between a file and a product. A buyer who opens a PDF and sees a title page, styled headings, and a working table reads it differently than one who sees raw text, even if the content is identical.
The styling rules I settled on
I went through about a dozen iterations. What stuck:
- One accent color, used for headings and rules only. Not per-heading rainbow palettes.
- Body text at 10.5pt with generous leading, because most buyers read these on laptops, not printed.
- Tables get alternating row shading. My metadata guides are full of the Adobe 5-column and Vecteezy 4-column CSV headers, and shaded rows make column boundaries legible without vertical lines.
- Page numbers in the footer. Sounds trivial. The first version had none and a buyer asked which page the license terms were on.
None of this is clever. It is just decisions made once, in a stylesheet, so every product ships the same way.
The memory lesson, because it bit me
My pipeline runs on a 1 vCPU, 2 GB RAM box with no GPU. The first time I batch-converted a set of PDFs, the process climbed past 1.6 GB and the OOM killer took it down mid-run. Three fixes solved it permanently:
- One document at a time, never parallel conversion on a single core.
-
gc.collect()after each file. - Checking peak memory with
resource.getrusage(RUSAGE_SELF).ru_maxrssinstead of guessing. Average RSS told me nothing useful. Peak told me exactly where the ceiling was.
For heavy render steps I now run one file per subprocess so the OS reclaims the memory when it exits. And when I inspect output, I render a preview at 60 dpi instead of 300. Same information, a fraction of the RAM.
Where this fits in the pipeline
md2pdf.py is one of four scripts that took my catalogue from raw assets to finished products. make_metadata.py writes the exact CSV headers Adobe (5 columns), Vecteezy (4 columns), and Dreamstime (15 columns) reject you for getting wrong. upload_tracker.py is a ledger so a re-run never double-posts, which is the fastest way to get an account flagged. pack_product.py zips the files and renders a 1280×720 cover plus a 600×600 thumbnail. Together those four packaged 39 products, about 856 MB of output, all on that 2 GB server.
The PDF converter was the last piece I built because I underestimated it. I thought formatting was cosmetic. It is not. A styled PDF is the packaging for anything document-shaped you sell, and buyers judge packaging before content.
The honest part
md2pdf.py will not design a book for you. It produces clean, consistent, professional output from Markdown. If you need custom layouts per product, you will edit the stylesheet, which is exactly what I do when a product warrants it. It is a tool, not a designer.
I bundled it with the other three scripts, the folder layout that keeps a catalogue from becoming a junk drawer, and the platform rules behind the metadata headers into the compliance metadata kit. It is the same tooling I run, on the same hardware constraints, with no cloud account and no framework.
Building digital products? Free guides & kits for digital product sellers.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.