diff options
Diffstat (limited to 'hp41_program_to_pdf.py')
| -rwxr-xr-x | hp41_program_to_pdf.py | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/hp41_program_to_pdf.py b/hp41_program_to_pdf.py new file mode 100755 index 0000000..630a5b6 --- /dev/null +++ b/hp41_program_to_pdf.py @@ -0,0 +1,182 @@ +#!/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: <input>.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()) |
