#!/usr/bin/env python3 import sys from pathlib import Path import textwrap import re try: from reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas from reportlab.lib.units import inch from reportlab.lib import colors except Exception as e: print('Missing reportlab:', e) sys.exit(2) def wrap_text(text, max_chars): return textwrap.wrap(text, width=max_chars) def draw_wrapped(c, text, x, y, font_name, font_size, max_width, indent=0): # Estimate chars per line roughly approx_char_width = font_size * 0.55 max_chars = max(20, int((max_width - indent) / approx_char_width)) wrapped = wrap_text(text, max_chars) line_height = font_size * 1.25 for line in wrapped: c.drawString(x + indent, y, line) y -= line_height return y #!/usr/bin/env python3 import sys from pathlib import Path import textwrap try: from reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas from reportlab.lib.units import inch from reportlab.lib import colors except Exception as e: print('Missing reportlab:', e) sys.exit(2) def wrap_text(text, max_chars): return textwrap.wrap(text, width=max_chars) def draw_wrapped(c, text, x, y, font_name, font_size, max_width, indent=0): # Estimate chars per line roughly approx_char_width = font_size * 0.55 max_chars = max(20, int((max_width - indent) / approx_char_width)) wrapped = wrap_text(text, max_chars) line_height = font_size * 1.25 for line in wrapped: c.drawString(x + indent, y, line) y -= line_height return y if len(sys.argv) < 3: print('Usage: md_to_pdf.py input.md output.pdf') sys.exit(1) in_path = Path(sys.argv[1]) out_path = Path(sys.argv[2]) if not in_path.exists(): print('Input file not found:', in_path) sys.exit(1) text = in_path.read_text(encoding='utf-8') lines = text.splitlines() c = canvas.Canvas(str(out_path), pagesize=letter) width, height = letter left = inch * 0.75 right = inch * 0.75 top = height - inch * 0.75 bottom = inch * 0.75 max_width = width - left - right # default fonts REGULAR = 'Helvetica' BOLD = 'Helvetica-Bold' MONO = 'Courier' y = top in_code = False code_font_size = 8 para_font_size = 10 for raw in lines: line = raw.rstrip('\n') if line.strip() == '': y -= para_font_size * 0.6 if y < bottom: c.showPage() y = top continue # Code fence toggle if line.strip().startswith('```'): in_code = not in_code if in_code: y -= 6 else: y -= 6 if y < bottom: c.showPage() y = top continue if in_code: c.setFont(MONO, code_font_size) # draw code line with small left indent y = draw_wrapped(c, line, left, y, MONO, code_font_size, max_width, indent=10) c.setFont(REGULAR, para_font_size) if y < bottom: c.showPage() y = top continue # Headings if line.lstrip().startswith('#'): hashes, _, rest = line.partition(' ') level = hashes.count('#') text = rest.strip() # add extra space BEFORE top-level headings so they don't butt against previous paragraph if level == 1: y -= para_font_size * 1.8 elif level == 2: y -= para_font_size * 0.8 else: y -= para_font_size * 0.6 if level == 1: font_size = 18 font = BOLD elif level == 2: font_size = 14 font = BOLD else: font_size = 12 font = BOLD c.setFont(font, font_size) y = draw_wrapped(c, text, left, y, font, font_size, max_width) # modest spacing after headings y -= para_font_size * 0.6 c.setFont(REGULAR, para_font_size) if y < bottom: c.showPage() y = top continue # Horizontal rule if line.strip() in ('---', '***', '___'): c.setStrokeColor(colors.black) c.setLineWidth(1) y -= 6 c.line(left, y, left + max_width, y) y -= 12 if y < bottom: c.showPage() y = top continue # Lists stripped = line.lstrip() if stripped.startswith(('- ', '* ', '+ ')) or stripped[:2].isdigit() and stripped[2:].startswith('. '): bullet = '•' content = stripped[2:].strip() if not stripped[0].isdigit() else stripped.split('.', 1)[1].strip() c.setFont(REGULAR, para_font_size) # draw bullet and wrapped content with indent c.drawString(left, y, bullet) y = draw_wrapped(c, content, left + 12, y, REGULAR, para_font_size, max_width, indent=0) if y < bottom: c.showPage() y = top continue # Paragraph c.setFont(REGULAR, para_font_size) y = draw_wrapped(c, line, left, y, REGULAR, para_font_size, max_width) if y < bottom: c.showPage() y = top c.save() print('Wrote PDF:', out_path)