#!/usr/bin/env python3 from pathlib import Path import argparse import pandas as pd BASE_DIR = Path(__file__).resolve().parents[1] DATA_DIR = BASE_DIR / "data" OUTPUT_DIR = BASE_DIR / "output" def clean_value(v): if pd.isna(v): return "" return str(v).strip() def bci_escape(v): v = clean_value(v) v = v.replace("\\", "\\\\") v = v.replace("'", "\\'") v = v.replace("\n", "\\n") return v def build_column_dict(row, exclude_cols): data = {} for col in row.index: if col in exclude_cols: continue val = clean_value(row[col]) if val != "": data[col] = val return data def dict_to_bci_payload(d): parts = [] for k, v in d.items(): parts.append(f"'{bci_escape(k)}':'{bci_escape(v)}'") return "{" + ",".join(parts) + "}" def main(): parser = argparse.ArgumentParser(description="Generate Bluebeam BCI script from Markups CSV + update CSV.") parser.add_argument("--bb-csv", default=str(DATA_DIR / "bluebeam_markups.csv"), help="Bluebeam Markups List CSV export") parser.add_argument("--updates-csv", default=str(DATA_DIR / "bluebeam_updates.csv"), help="Your update CSV") parser.add_argument("--out", default=str(OUTPUT_DIR / "update_columns.bci"), help="Output BCI file") parser.add_argument("--pdf-path", default=r"C:\PATH\TO\TARGET.pdf", help="Windows path to target PDF for the BCI Open() command") parser.add_argument("--match-column", default="Comment", help="Column in Bluebeam CSV to match against match_value") parser.add_argument("--page-column", default="Page Index", help="Bluebeam page index column") parser.add_argument("--id-column", default="ID", help="Bluebeam markup ID column") parser.add_argument("--contains", action="store_true", help="Use contains match instead of exact match") args = parser.parse_args() bb_csv = Path(args.bb_csv) updates_csv = Path(args.updates_csv) out_path = Path(args.out) OUTPUT_DIR.mkdir(exist_ok=True) bb = pd.read_csv(bb_csv, dtype=str).fillna("") upd = pd.read_csv(updates_csv, dtype=str).fillna("") required_bb = {args.page_column, args.id_column, args.match_column} missing_bb = required_bb - set(bb.columns) if missing_bb: raise ValueError(f"Bluebeam CSV missing required columns: {sorted(missing_bb)}") required_upd = {"match_value"} missing_upd = required_upd - set(upd.columns) if missing_upd: raise ValueError(f"Updates CSV missing required columns: {sorted(missing_upd)}") lines = [] lines.append(f"Open('{args.pdf_path}', '')") total_matches = 0 for _, urow in upd.iterrows(): match_value = clean_value(urow["match_value"]) if not match_value: continue if args.contains: mask = bb[args.match_column].astype(str).str.contains(match_value, na=False, regex=False) else: mask = bb[args.match_column].astype(str).str.strip() == match_value matches = bb[mask] col_data = build_column_dict(urow, exclude_cols={"match_value"}) if not col_data: continue payload = dict_to_bci_payload(col_data) for _, brow in matches.iterrows(): page_index = clean_value(brow[args.page_column]) markup_id = clean_value(brow[args.id_column]) if page_index == "" or markup_id == "": continue lines.append(f'ColumnDataSet({page_index},"{markup_id}","{payload}")') total_matches += 1 lines.append("Save()") lines.append("Close()") out_path.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"WROTE: {out_path}") print(f"MATCHED MARKUPS: {total_matches}") if __name__ == "__main__": main()