#!/usr/bin/env python3 """ Render a Verkada POC Criteria markdown document as a formatted .odt file. This script takes a POC Criteria markdown file (generated by the pov-doc skill), replaces logo placeholders and mission statement with actual content, and converts the result to a professionally formatted .odt using pandoc with a reference document. Usage: python render_poc.py --input --customer-logo \ [--verkada-logo ] [--mission <"mission statement">] \ [--reference-doc ] [--output ] If --verkada-logo is not provided, the Verkada logo placeholder is removed. If --mission is not provided, the existing mission statement in the markdown is kept. If --reference-doc is not provided, defaults to the included City of El Paso reference. If --output is not provided, defaults to the input filename with .odt extension. Dependencies: - pandoc (must be installed and on PATH) - Pillow (for logo resizing) """ import argparse import os import re import subprocess import sys import tempfile import zipfile from pathlib import Path def find_skill_dir() -> Path: """Locate the pov-doc skill directory relative to this script.""" return Path(__file__).resolve().parent.parent def find_reference_doc() -> Path: """Find the default reference ODT template in the skill's examples directory.""" skill_dir = find_skill_dir() ref = skill_dir / "examples" / "POC Reference Template.odt" if ref.exists(): return ref # Fallback: look for the El Paso doc and warn alt = skill_dir / "examples" / "City of El Paso POC Criteria.odt" if alt.exists(): print( "Warning: using City of El Paso reference (may carry over logos). " "Run create_reference_template() to generate a clean template.", file=sys.stderr, ) return alt return None def find_verkada_logo() -> Path: """Find the bundled Verkada logo in the skill's assets directory.""" skill_dir = find_skill_dir() logo = skill_dir / "assets" / "verkada-logo.png" if logo.exists(): return logo return None def resize_logo_if_needed( input_path: str, max_width_inches: float = 2.75, dpi: int = 150 ) -> str: """ Resize a logo image if it exceeds the target width for print. Returns the path to the (possibly new) resized image. """ try: from PIL import Image img = Image.open(input_path) width_px, height_px = img.size max_width_px = int(max_width_inches * dpi) if width_px > max_width_px: ratio = max_width_px / width_px new_size = (max_width_px, int(height_px * ratio)) img_resized = img.resize(new_size, Image.LANCZOS) # Write resized version to a temp file fd, out_path = tempfile.mkstemp(suffix=".png") os.close(fd) img_resized.save(out_path, "PNG") return out_path return input_path except ImportError: print("Warning: Pillow not installed, skipping logo resize", file=sys.stderr) return input_path def process_markdown( md_content: str, customer_logo_path: str, verkada_logo_path: str | None, mission_statement: str | None, ) -> str: """ Process the markdown content: 1. Replace logo HTML comments with markdown image syntax 2. Optionally update the mission statement 3. Remove the Table of Contents section (pandoc can generate one) """ # Replace customer logo placeholder (match any logo comment that is NOT Verkada) md_content = re.sub( r"\s*\n?", f"![Customer Logo]({customer_logo_path})\n\n", md_content, flags=re.IGNORECASE, ) # Replace Verkada logo placeholder if verkada_logo_path: md_content = re.sub( r"\s*\n?", f"![Verkada Logo]({verkada_logo_path})\n", md_content, flags=re.IGNORECASE, ) else: # Remove the Verkada logo placeholder entirely md_content = re.sub( r"\s*\n?", "", md_content, flags=re.IGNORECASE, ) # Update mission statement if provided if mission_statement: # Match the existing italicized mission statement md_content = re.sub( r'\*"[^"]*"\*\s*\n', f'*"{mission_statement}"*\n\n', md_content, ) return md_content def convert_with_pandoc(md_path: str, output_path: str, reference_doc: str) -> bool: """Convert markdown to .odt using pandoc with a reference document.""" cmd = [ "pandoc", md_path, "-o", output_path, "--from", "markdown", "--to", "odt", "--reference-doc", reference_doc, ] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode != 0: print(f"pandoc error: {result.stderr}", file=sys.stderr) return False return True except FileNotFoundError: print( "Error: pandoc not found. Install it with your package manager.", file=sys.stderr, ) return False except subprocess.TimeoutExpired: print("Error: pandoc timed out", file=sys.stderr) return False def _remove_elements_from_xml( data: bytes, xpath_to_remove: list[tuple[str, dict]] ) -> bytes: """Remove elements matching the given xpath patterns from ODT XML data.""" import xml.etree.ElementTree as ET tree = ET.fromstring(data) changed = False for xpath, ns in xpath_to_remove: for el in tree.findall(xpath, ns): for parent in tree.iter(): if el in list(parent): parent.remove(el) changed = True break if changed: return ET.tostring(tree, encoding="unicode", xml_declaration=True).encode( "utf-8" ) return data def _odt_ns() -> dict: return { "text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0", "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0", "style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0", "draw": "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0", } def remove_heading_bookmarks(odt_path: str) -> None: """Remove bookmark-start and bookmark-end elements from the ODT content.xml.""" ns = _odt_ns() bookmarks_xpath = [ (".//text:bookmark", ns), (".//text:bookmark-start", ns), (".//text:bookmark-end", ns), ] tmp_path = odt_path + ".tmp" with zipfile.ZipFile(odt_path, "r") as zin: with zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as zout: for item in zin.infolist(): data = zin.read(item.filename) if item.filename == "content.xml": data = _remove_elements_from_xml(data, bookmarks_xpath) zout.writestr(item, data) os.replace(tmp_path, odt_path) def remove_header_images(odt_path: str) -> None: """Remove draw:frame elements from the header in styles.xml to strip reference doc logos.""" ns = _odt_ns() frames_xpath = [ (".//style:header//draw:frame", ns), (".//style:footer//draw:frame", ns), ] tmp_path = odt_path + ".tmp" with zipfile.ZipFile(odt_path, "r") as zin: with zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as zout: for item in zin.infolist(): data = zin.read(item.filename) if item.filename == "styles.xml": data = _remove_elements_from_xml(data, frames_xpath) zout.writestr(item, data) os.replace(tmp_path, odt_path) def main(): parser = argparse.ArgumentParser( description="Render a Verkada POC Criteria markdown document as a formatted .odt file." ) parser.add_argument( "--input", "-i", required=True, help="Path to the POC Criteria markdown file" ) parser.add_argument( "--customer-logo", required=True, help="Path to the customer logo image" ) parser.add_argument( "--verkada-logo", help="Path to the Verkada logo image (optional)" ) parser.add_argument( "--mission", help="Customer mission statement (replaces existing)" ) parser.add_argument("--reference-doc", help="Path to reference .odt for formatting") parser.add_argument( "--output", "-o", help="Output .odt path (default: same as input with .odt)" ) args = parser.parse_args() # Validate input file input_path = Path(args.input) if not input_path.exists(): print(f"Error: input file not found: {input_path}", file=sys.stderr) sys.exit(1) # Validate customer logo if not os.path.isfile(args.customer_logo): print(f"Error: customer logo not found: {args.customer_logo}", file=sys.stderr) sys.exit(1) # Find reference doc reference_doc = args.reference_doc or find_reference_doc() if not reference_doc or not Path(reference_doc).exists(): print( "Error: reference document not found. Provide --reference-doc or place the " "City of El Paso POC Criteria.odt in the skill's examples directory.", file=sys.stderr, ) sys.exit(1) # Find Verkada logo verkada_logo = args.verkada_logo or find_verkada_logo() if verkada_logo and not Path(verkada_logo).exists(): verkada_logo = None # Determine output path if args.output: output_path = args.output else: output_path = str(input_path.with_suffix(".odt")) # Resize logos for print customer_logo_resized = resize_logo_if_needed(args.customer_logo) verkada_logo_resized = resize_logo_if_needed(verkada_logo) if verkada_logo else None # Read and process markdown md_content = input_path.read_text(encoding="utf-8") processed = process_markdown( md_content, customer_logo_resized, verkada_logo_resized, args.mission, ) # Write processed markdown to temp file (so image paths work) fd, temp_md = tempfile.mkstemp(suffix=".md") os.close(fd) try: with open(temp_md, "w", encoding="utf-8") as f: f.write(processed) # Convert with pandoc print(f"Converting {input_path.name} to {output_path}...") print(f" Reference doc: {reference_doc}") if verkada_logo: print(f" Verkada logo: {verkada_logo}") success = convert_with_pandoc(temp_md, output_path, reference_doc) if success: remove_heading_bookmarks(output_path) remove_header_images(output_path) out_size = os.path.getsize(output_path) print(f"Done: {output_path} ({out_size:,} bytes)") else: sys.exit(1) finally: # Clean up temp files os.unlink(temp_md) if customer_logo_resized != args.customer_logo: os.unlink(customer_logo_resized) if verkada_logo_resized and verkada_logo_resized != verkada_logo: os.unlink(verkada_logo_resized) if __name__ == "__main__": main()