diff --git a/pov-doc/SKILL.md b/pov-doc/SKILL.md index 05ab8e2..1f14f91 100644 --- a/pov-doc/SKILL.md +++ b/pov-doc/SKILL.md @@ -142,6 +142,4 @@ Review the markdown examples to match tone, structure, and formatting convention ## Output -Write the generated markdown document to `~/notes/Inbox/` with the filename format `[Customer Name] POC Criteria.md`. - -After rendering, the .odt file should be placed alongside the markdown or in the same location the user specifies. +Write both the generated markdown and the rendered .odt to `~/notes/Inbox/agent/`. Use the filename format `[Customer Name] POC Criteria.md` and `[Customer Name] POC Criteria.odt`. diff --git a/pov-doc/examples/City of El Paso POC Criteria.odt b/pov-doc/examples/City of El Paso POC Criteria.odt deleted file mode 100644 index 02a1db5..0000000 Binary files a/pov-doc/examples/City of El Paso POC Criteria.odt and /dev/null differ diff --git a/pov-doc/examples/Proof of Concept Criteria - TEMPLATE.odt b/pov-doc/examples/Proof of Concept Criteria - TEMPLATE.odt deleted file mode 100644 index 1427ef2..0000000 Binary files a/pov-doc/examples/Proof of Concept Criteria - TEMPLATE.odt and /dev/null differ diff --git a/pov-doc/scripts/render_poc.py b/pov-doc/scripts/render_poc.py index 180075e..f579265 100644 --- a/pov-doc/scripts/render_poc.py +++ b/pov-doc/scripts/render_poc.py @@ -27,6 +27,7 @@ import re import subprocess import sys import tempfile +import zipfile from pathlib import Path @@ -44,9 +45,11 @@ def find_reference_doc() -> Path: # 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) + 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 @@ -60,13 +63,16 @@ def find_verkada_logo() -> Path: return None -def resize_logo_if_needed(input_path: str, max_width_inches: float = 2.75, dpi: int = 150) -> str: +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) @@ -103,8 +109,8 @@ def process_markdown( # 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', + r"\s*\n?", + f"![Customer Logo]({customer_logo_path})\n\n", md_content, flags=re.IGNORECASE, ) @@ -112,16 +118,16 @@ def process_markdown( # Replace Verkada logo placeholder if verkada_logo_path: md_content = re.sub( - r'\s*\n?', - f'![Verkada Logo]({verkada_logo_path})\n', + 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?', - '', + r"\s*\n?", + "", md_content, flags=re.IGNORECASE, ) @@ -143,10 +149,14 @@ def convert_with_pandoc(md_path: str, output_path: str, reference_doc: str) -> b cmd = [ "pandoc", md_path, - "-o", output_path, - "--from", "markdown", - "--to", "odt", - "--reference-doc", reference_doc, + "-o", + output_path, + "--from", + "markdown", + "--to", + "odt", + "--reference-doc", + reference_doc, ] try: @@ -156,23 +166,108 @@ def convert_with_pandoc(md_path: str, output_path: str, reference_doc: str) -> b return False return True except FileNotFoundError: - print("Error: pandoc not found. Install it with your package manager.", file=sys.stderr) + 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( + "--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)") + parser.add_argument( + "--output", "-o", help="Output .odt path (default: same as input with .odt)" + ) args = parser.parse_args() @@ -190,9 +285,11 @@ def main(): # 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) + 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 @@ -235,6 +332,8 @@ def main(): 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: