summaryrefslogtreecommitdiff
path: root/hp41_program_to_pdf.py
diff options
context:
space:
mode:
Diffstat (limited to 'hp41_program_to_pdf.py')
-rwxr-xr-xhp41_program_to_pdf.py182
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())