From 664e8b44fb4a2265bd6237be2d3b40188546ab93 Mon Sep 17 00:00:00 2001 From: McElwain Date: Thu, 7 May 2026 13:03:18 -0500 Subject: [PATCH] update app --- app/bluebeam_bci.py | 112 +++++++++++++++++++++++++++++++++++++++++ app/update_existing.py | 36 +++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 app/bluebeam_bci.py create mode 100644 app/update_existing.py diff --git a/app/bluebeam_bci.py b/app/bluebeam_bci.py new file mode 100644 index 0000000..2984dc3 --- /dev/null +++ b/app/bluebeam_bci.py @@ -0,0 +1,112 @@ +#!/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() diff --git a/app/update_existing.py b/app/update_existing.py new file mode 100644 index 0000000..2dab5a6 --- /dev/null +++ b/app/update_existing.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +from pathlib import Path +import fitz + +BASE_DIR = Path(__file__).resolve().parents[1] +INPUT = BASE_DIR / "input" / "okular_test.pdf" +OUTPUT = BASE_DIR / "output" / "okular_test_updated.pdf" + +OLD_TEXT = "TITLE TEXT HERE" +NEW_TEXT = "UPDATED TITLE TEXT\nLINE 2 UPDATED\nLINE 3 UPDATED" + +doc = fitz.open(INPUT) + +changed = 0 + +for page in doc: + for annot in page.annots() or []: + info = annot.info or {} + content = info.get("content", "") + + if OLD_TEXT in content: + info["content"] = NEW_TEXT + annot.set_info(info) + + # FreeText annotations need appearance regenerated + if annot.type[1] == "FreeText": + annot.update() + + changed += 1 + +doc.save(OUTPUT, garbage=4, deflate=True) +doc.close() + +print(f"Updated {changed} annotation(s)") +print(f"Wrote {OUTPUT}")