#!/usr/bin/env python3 """ Generate a PDF listing for an HP-41C program with line numbers and a header. Requires: reportlab (pip install reportlab) """ from __future__ import annotations import argparse import os import sys from datetime import datetime import re def require_reportlab(): try: from reportlab.lib.pagesizes import A4 # noqa: F401 from reportlab.lib.units import mm # noqa: F401 from reportlab.pdfgen import canvas # noqa: F401 except Exception as exc: # pragma: no cover print("Error: reportlab is required. Install via: pip install reportlab", file=sys.stderr) raise SystemExit(2) from exc def iter_wrapped_lines(text: str, max_width: float, font_name: str, font_size: int): from reportlab.pdfbase import pdfmetrics if not text: return [""] words = text.split(" ") lines = [] current = "" for word in words: candidate = word if not current else f"{current} {word}" width = pdfmetrics.stringWidth(candidate, font_name, font_size) if width <= max_width: current = candidate else: if current: lines.append(current) current = word if current: lines.append(current) return lines def generate_pdf(lines: list[str], output_path: str, program_name: str): from reportlab.lib.pagesizes import A4 from reportlab.lib.units import mm from reportlab.pdfgen import canvas from reportlab.pdfbase import pdfmetrics page_width, page_height = A4 margin_left = 18 * mm margin_right = 18 * mm margin_top = 18 * mm margin_bottom = 18 * mm header_font = "Helvetica-Bold" body_font = "Courier" header_size = 14 body_size = 10 line_height = 1.35 * body_size content_width = page_width - margin_left - margin_right usable_height = page_height - margin_top - margin_bottom - (header_size + 6) lines_per_page = int(usable_height // line_height) if lines_per_page <= 0: raise ValueError("Page layout too tight. Increase margins or reduce font size.") c = canvas.Canvas(output_path, pagesize=A4) c.setAuthor("hp41_program_to_pdf.py") c.setTitle(program_name) timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") total_lines = len(lines) line_number = 1 page_number = 1 def draw_header(): c.setFont(header_font, header_size) c.drawString(margin_left, page_height - margin_top, program_name) c.setFont("Helvetica", 8) c.drawRightString(page_width - margin_right, page_height - margin_top + 2, timestamp) c.drawRightString(page_width - margin_right, page_height - margin_top - 10, f"Page {page_number}") c.line(margin_left, page_height - margin_top - 14, page_width - margin_right, page_height - margin_top - 14) y_start = page_height - margin_top - 24 y = y_start draw_header() c.setFont(body_font, body_size) # Pre-parse line numbers when present; keep blank/comment lines but don't # count them for numbering. parsed_lines: list[tuple[int | None, str]] = [] auto_line_no = 1 number_re = re.compile(r"^\s*(\d+)[\s:]+(.*)$") for original in lines: stripped = original.strip() if stripped == "" or stripped.startswith("#"): parsed_lines.append((None, original)) continue match = number_re.match(original) if match: num = int(match.group(1)) text = match.group(2).rstrip() parsed_lines.append((num, text)) auto_line_no = num + 1 else: parsed_lines.append((auto_line_no, original)) auto_line_no += 1 numbered = [n for n, _ in parsed_lines if n is not None] if numbered: max_digits = max(len(str(n)) for n in numbered) else: max_digits = 1 line_no_format = f"{{:0{max_digits}d}}" line_no_width = pdfmetrics.stringWidth("0" * max_digits, body_font, body_size) + 6 content_width = page_width - margin_left - margin_right - line_no_width for line_number, original in parsed_lines: wrapped = iter_wrapped_lines(original, content_width, body_font, body_size) for i, wline in enumerate(wrapped): if y < margin_bottom + line_height: c.showPage() page_number += 1 draw_header() c.setFont(body_font, body_size) y = y_start if i == 0 and line_number is not None: c.drawString(margin_left, y, line_no_format.format(line_number)) c.drawString(margin_left + line_no_width, y, wline) y -= line_height c.save() if total_lines == 0: print("Warning: input file had no lines.", file=sys.stderr) def main() -> int: require_reportlab() parser = argparse.ArgumentParser( description="Generate a PDF listing for an HP-41C program." ) parser.add_argument("input", help="Text file with HP-41C program steps, one per line.") parser.add_argument( "output", nargs="?", help="Output PDF file (default: .pdf).", ) parser.add_argument( "--name", help="Program name to use in the header (default: input filename).", ) args = parser.parse_args() input_path = args.input if not os.path.isfile(input_path): print(f"Error: input file not found: {input_path}", file=sys.stderr) return 2 output_path = args.output or f"{input_path}.pdf" program_name = args.name or os.path.splitext(os.path.basename(input_path))[0] with open(input_path, "r", encoding="utf-8") as f: lines = [line.rstrip("\n") for line in f.readlines()] generate_pdf(lines, output_path, program_name) print(f"Wrote {output_path}") return 0 if __name__ == "__main__": raise SystemExit(main())