diff --git a/composite-envelope-builder/scripts/md_to_pdf.py b/composite-envelope-builder/scripts/md_to_pdf.py new file mode 100644 index 0000000..a42ab09 --- /dev/null +++ b/composite-envelope-builder/scripts/md_to_pdf.py @@ -0,0 +1,192 @@ +#!/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)